changeset 673:d62fd61082a1

Merge from branch tuning-difference
author Chris Cannam
date Fri, 17 May 2019 09:46:22 +0100
parents e19c609a7bec (current diff) ae7584dbd668 (diff)
children b375fdbb74bc
files
diffstat 10 files changed, 998 insertions(+), 437 deletions(-) [+]
line wrap: on
line diff
--- a/audio/AudioGenerator.cpp	Thu Apr 04 16:17:11 2019 +0100
+++ b/audio/AudioGenerator.cpp	Fri May 17 09:46:22 2019 +0100
@@ -22,11 +22,10 @@
 #include "base/Exceptions.h"
 
 #include "data/model/NoteModel.h"
-#include "data/model/FlexiNoteModel.h"
 #include "data/model/DenseTimeValueModel.h"
 #include "data/model/SparseTimeValueModel.h"
 #include "data/model/SparseOneDimensionalModel.h"
-#include "data/model/NoteData.h"
+#include "base/NoteData.h"
 
 #include "ClipMixer.h"
 #include "ContinuousSynth.h"
@@ -185,8 +184,7 @@
 {
     bool clip = 
         (qobject_cast<const SparseOneDimensionalModel *>(model) ||
-         qobject_cast<const NoteModel *>(model) ||
-         qobject_cast<const FlexiNoteModel *>(model));
+         qobject_cast<const NoteModel *>(model));
     return clip;
 }
 
@@ -196,9 +194,7 @@
     // basically, anything that usually has sustain (like notes) or
     // often has multiple sounds at once (like notes) wants to use a
     // quieter level than simple click tracks
-    bool does = 
-        (qobject_cast<const NoteModel *>(model) ||
-         qobject_cast<const FlexiNoteModel *>(model));
+    bool does = (qobject_cast<const NoteModel *>(model));
     return does;
 }
 
@@ -559,6 +555,8 @@
 
     float **bufferIndexes = new float *[m_targetChannelCount];
 
+    //!!! + for first block, prime with notes already active
+    
     for (int i = 0; i < blocks; ++i) {
 
         sv_frame_t reqStart = startFrame + i * m_processingBlockSize;
@@ -566,8 +564,8 @@
         NoteList notes;
         NoteExportable *exportable = dynamic_cast<NoteExportable *>(model);
         if (exportable) {
-            notes = exportable->getNotesWithin(reqStart,
-                                               reqStart + m_processingBlockSize);
+            notes = exportable->getNotesStartingWithin(reqStart,
+                                                       m_processingBlockSize);
         }
 
         std::vector<ClipMixer::NoteStart> starts;
@@ -714,32 +712,28 @@
             bufferIndexes[c] = buffer[c] + i * m_processingBlockSize;
         }
 
-        SparseTimeValueModel::PointList points = 
-            stvm->getPoints(reqStart, reqStart + m_processingBlockSize);
+        EventVector points = 
+            stvm->getEventsStartingWithin(reqStart, m_processingBlockSize);
 
         // by default, repeat last frequency
         float f0 = 0.f;
 
-        // go straight to the last freq that is genuinely in this range
-        for (SparseTimeValueModel::PointList::const_iterator itr = points.end();
-             itr != points.begin(); ) {
-            --itr;
-            if (itr->frame >= reqStart &&
-                itr->frame < reqStart + m_processingBlockSize) {
-                f0 = itr->value;
-                break;
-            }
+        // go straight to the last freq in this range
+        if (!points.empty()) {
+            f0 = points.rbegin()->getValue();
         }
 
-        // if we found no such frequency and the next point is further
+        // if there is no such frequency and the next point is further
         // away than twice the model resolution, go silent (same
         // criterion TimeValueLayer uses for ending a discrete curve
         // segment)
         if (f0 == 0.f) {
-            SparseTimeValueModel::PointList nextPoints = 
-                stvm->getNextPoints(reqStart + m_processingBlockSize);
-            if (nextPoints.empty() ||
-                nextPoints.begin()->frame > reqStart + 2 * stvm->getResolution()) {
+            Event nextP;
+            if (!stvm->getNearestEventMatching(reqStart + m_processingBlockSize,
+                                               [](Event) { return true; },
+                                               EventSeries::Forward,
+                                               nextP) ||
+                nextP.getFrame() > reqStart + 2 * stvm->getResolution()) {
                 f0 = -1.f;
             }
         }
--- a/files.pri	Thu Apr 04 16:17:11 2019 +0100
+++ b/files.pri	Fri May 17 09:46:22 2019 +0100
@@ -9,6 +9,7 @@
            framework/Align.h \
 	   framework/Document.h \
            framework/MainWindowBase.h \
+           framework/OSCScript.h \
            framework/SVFileReader.h \
            framework/TransformUserConfigurator.h \
            framework/VersionTester.h
--- a/framework/Align.cpp	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/Align.cpp	Fri May 17 09:46:22 2019 +0100
@@ -4,7 +4,6 @@
     Sonic Visualiser
     An audio file viewer and annotation editor.
     Centre for Digital Music, Queen Mary, University of London.
-    This file copyright 2006 Chris Cannam and QMUL.
     
     This program is free software; you can redistribute it and/or
     modify it under the terms of the GNU General Public License as
@@ -34,7 +33,7 @@
 #include <QApplication>
 
 bool
-Align::alignModel(Document *doc, Model *ref, Model *other)
+Align::alignModel(Document *doc, Model *ref, Model *other, QString &error)
 {
     QSettings settings;
     settings.beginGroup("Preferences");
@@ -43,9 +42,9 @@
     settings.endGroup();
 
     if (useProgram && (program != "")) {
-        return alignModelViaProgram(doc, ref, other, program);
+        return alignModelViaProgram(doc, ref, other, program, error);
     } else {
-        return alignModelViaTransform(doc, ref, other);
+        return alignModelViaTransform(doc, ref, other, error);
     }
 }
 
@@ -61,17 +60,40 @@
     return id;
 }
 
+QString
+Align::getTuningDifferenceTransformName()
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    bool performPitchCompensation =
+        settings.value("align-pitch-aware", false).toBool();
+    QString id = "";
+    if (performPitchCompensation) {
+        id = settings.value
+            ("tuning-difference-transform-id",
+             "vamp:tuning-difference:tuning-difference:tuningfreq")
+            .toString();
+    }
+    settings.endGroup();
+    return id;
+}
+
 bool
 Align::canAlign() 
 {
+    TransformFactory *factory = TransformFactory::getInstance();
     TransformId id = getAlignmentTransformName();
-    TransformFactory *factory = TransformFactory::getInstance();
-    return factory->haveTransform(id);
+    TransformId tdId = getTuningDifferenceTransformName();
+    return factory->haveTransform(id) &&
+        (tdId == "" || factory->haveTransform(tdId));
 }
 
 bool
-Align::alignModelViaTransform(Document *doc, Model *ref, Model *other)
+Align::alignModelViaTransform(Document *doc, Model *ref, Model *other,
+                              QString &error)
 {
+    QMutexLocker locker (&m_mutex);
+    
     RangeSummarisableTimeValueModel *reference = qobject_cast
         <RangeSummarisableTimeValueModel *>(ref);
     
@@ -80,24 +102,39 @@
 
     if (!reference || !rm) return false; // but this should have been tested already
    
-    // This involves creating three new models:
-
+    // This involves creating either three or four new models:
+    //
     // 1. an AggregateWaveModel to provide the mixdowns of the main
     // model and the new model in its two channels, as input to the
     // MATCH plugin
-
-    // 2. a SparseTimeValueModel, which is the model automatically
-    // created by FeatureExtractionPluginTransformer when running the
-    // MATCH plugin (thus containing the alignment path)
-
+    //
+    // 2a. a SparseTimeValueModel which will be automatically created
+    // by FeatureExtractionModelTransformer when running the
+    // TuningDifference plugin to receive the relative tuning of the
+    // second model (if pitch-aware alignment is enabled in the
+    // preferences)
+    //
+    // 2b. a SparseTimeValueModel which will be automatically created
+    // by FeatureExtractionPluginTransformer when running the MATCH
+    // plugin to perform alignment (so containing the alignment path)
+    //
     // 3. an AlignmentModel, which stores the path model and carries
     // out alignment lookups on it.
-
-    // The first two of these are provided as arguments to the
-    // constructor for the third, which takes responsibility for
-    // deleting them.  The AlignmentModel, meanwhile, is passed to the
-    // new model we are aligning, which also takes responsibility for
-    // it.  We should not have to delete any of these new models here.
+    //
+    // The AggregateWaveModel [1] is registered with the document,
+    // which deletes it when it is invalidated (when one of its
+    // components is deleted). The SparseTimeValueModel [2a] is reused
+    // by us when starting the alignment process proper, and is then
+    // deleted by us. The SparseTimeValueModel [2b] is passed to the
+    // AlignmentModel, which takes ownership of it. The AlignmentModel
+    // is attached to the new model we are aligning, which also takes
+    // ownership of it. The only one of these models that we need to
+    // delete here is the SparseTimeValueModel [2a].
+    //
+    // (We also create a sneaky additional SparseTimeValueModel
+    // temporarily so we can attach completion information to it -
+    // this is quite unnecessary from the perspective of simply
+    // producing the results.)
 
     AggregateWaveModel::ChannelSpecList components;
 
@@ -109,9 +146,131 @@
 
     AggregateWaveModel *aggregateModel = new AggregateWaveModel(components);
     doc->addAggregateModel(aggregateModel);
+
+    AlignmentModel *alignmentModel =
+        new AlignmentModel(reference, other, nullptr);
+
+    TransformId tdId = getTuningDifferenceTransformName();
+
+    if (tdId == "") {
+        
+        if (beginTransformDrivenAlignment(aggregateModel, alignmentModel)) {
+            rm->setAlignment(alignmentModel);
+        } else {
+            error = alignmentModel->getError();
+            delete alignmentModel;
+            return false;
+        }
+
+    } else {
+
+        // Have a tuning-difference transform id, so run it
+        // asynchronously first
+        
+        TransformFactory *tf = TransformFactory::getInstance();
+
+        Transform transform = tf->getDefaultTransformFor
+            (tdId, aggregateModel->getSampleRate());
+
+        transform.setParameter("maxduration", 50);
+        transform.setParameter("maxrange", 5);
     
-    ModelTransformer::Input aggregate(aggregateModel);
+        SVDEBUG << "Align::alignModel: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
 
+        ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
+
+        QString message;
+        Model *transformOutput = mtf->transform(transform, aggregateModel, message);
+
+        SparseTimeValueModel *tdout = dynamic_cast<SparseTimeValueModel *>
+            (transformOutput);
+        
+        if (!tdout) {
+            SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
+            delete tdout;
+            error = message;
+            return false;
+        }
+
+        rm->setAlignment(alignmentModel);
+    
+        connect(tdout, SIGNAL(completionChanged()),
+                this, SLOT(tuningDifferenceCompletionChanged()));
+
+        TuningDiffRec rec;
+        rec.input = aggregateModel;
+        rec.alignment = alignmentModel;
+
+        // This model exists only so that the AlignmentModel can get a
+        // completion value from somewhere while the tuning difference
+        // calculation is going on
+        rec.preparatory = new SparseTimeValueModel
+            (aggregateModel->getSampleRate(), 1);;
+        rec.preparatory->setCompletion(0);
+        alignmentModel->setPathFrom(rec.preparatory);
+        
+        m_pendingTuningDiffs[tdout] = rec;
+    }
+
+    return true;
+}
+
+void
+Align::tuningDifferenceCompletionChanged()
+{
+    QMutexLocker locker (&m_mutex);
+    
+    SparseTimeValueModel *td = qobject_cast<SparseTimeValueModel *>(sender());
+    if (!td) return;
+
+    if (m_pendingTuningDiffs.find(td) == m_pendingTuningDiffs.end()) {
+        SVCERR << "ERROR: Align::tuningDifferenceCompletionChanged: Model "
+               << td << " not found in pending tuning diff map!" << endl;
+        return;
+    }
+
+    TuningDiffRec rec = m_pendingTuningDiffs[td];
+
+    int completion = 0;
+    bool done = td->isReady(&completion);
+
+    SVCERR << "Align::tuningDifferenceCompletionChanged: done = " << done << ", completion = " << completion << endl;
+
+    if (!done) {
+        // This will be the completion the alignment model reports,
+        // before the alignment actually begins. It goes up from 0 to
+        // 99 (not 100!) and then back to 0 again when we start
+        // calculating the actual path in the following phase
+        int clamped = (completion == 100 ? 99 : completion);
+        SVCERR << "Align::tuningDifferenceCompletionChanged: setting rec.preparatory completion to " << clamped << endl;
+        rec.preparatory->setCompletion(clamped);
+        return;
+    }
+
+    float tuningFrequency = 440.f;
+    
+    if (!td->isEmpty()) {
+        tuningFrequency = td->getAllEvents()[0].getValue();
+        SVCERR << "Align::tuningDifferenceCompletionChanged: Reported tuning frequency = " << tuningFrequency << endl;
+    } else {
+        SVCERR << "Align::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
+    }    
+
+    m_pendingTuningDiffs.erase(td);
+    td->aboutToDelete();
+    delete td;
+
+    rec.alignment->setPathFrom(nullptr);
+    
+    beginTransformDrivenAlignment
+        (rec.input, rec.alignment, tuningFrequency);
+}
+
+bool
+Align::beginTransformDrivenAlignment(AggregateWaveModel *aggregateModel,
+                                     AlignmentModel *alignmentModel,
+                                     float tuningFrequency)
+{
     TransformId id = getAlignmentTransformName();
     
     TransformFactory *tf = TransformFactory::getInstance();
@@ -123,38 +282,42 @@
     transform.setParameter("serialise", 1);
     transform.setParameter("smooth", 0);
 
+    if (tuningFrequency != 0.f) {
+        transform.setParameter("freq2", tuningFrequency);
+    }
+
     SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
 
     ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
 
     QString message;
-    Model *transformOutput = mtf->transform(transform, aggregate, message);
+    Model *transformOutput = mtf->transform
+        (transform, aggregateModel, message);
 
     if (!transformOutput) {
         transform.setStepSize(0);
-        transformOutput = mtf->transform(transform, aggregate, message);
+        transformOutput = mtf->transform
+            (transform, aggregateModel, message);
     }
 
     SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *>
         (transformOutput);
 
+    //!!! callers will need to be updated to get error from
+    //!!! alignment model after initial call
+        
     if (!path) {
-        cerr << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
+        SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
         delete transformOutput;
-        delete aggregateModel;
-        m_error = message;
+        alignmentModel->setError(message);
         return false;
     }
 
     path->setCompletion(0);
-
-    AlignmentModel *alignmentModel = new AlignmentModel
-        (reference, other, path);
+    alignmentModel->setPathFrom(path);
 
     connect(alignmentModel, SIGNAL(completionChanged()),
             this, SLOT(alignmentCompletionChanged()));
-    
-    rm->setAlignment(alignmentModel);
 
     return true;
 }
@@ -162,6 +325,8 @@
 void
 Align::alignmentCompletionChanged()
 {
+    QMutexLocker locker (&m_mutex);
+    
     AlignmentModel *am = qobject_cast<AlignmentModel *>(sender());
     if (!am) return;
     if (am->isReady()) {
@@ -172,8 +337,11 @@
 }
 
 bool
-Align::alignModelViaProgram(Document *, Model *ref, Model *other, QString program)
+Align::alignModelViaProgram(Document *, Model *ref, Model *other,
+                            QString program, QString &error)
 {
+    QMutexLocker locker (&m_mutex);
+    
     WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref);
     WaveFileModel *rm = qobject_cast<WaveFileModel *>(other);
 
@@ -192,7 +360,7 @@
     ReadOnlyWaveFileModel *roref = qobject_cast<ReadOnlyWaveFileModel *>(reference);
     ReadOnlyWaveFileModel *rorm = qobject_cast<ReadOnlyWaveFileModel *>(rm);
     if (!roref || !rorm) {
-        cerr << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
+        SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
         return false;
     }
     
@@ -200,12 +368,10 @@
     QString otherPath = rorm->getLocalFilename();
 
     if (refPath == "" || otherPath == "") {
-        m_error = "Failed to find local filepath for wave-file model";
+        error = "Failed to find local filepath for wave-file model";
         return false;
     }
 
-    m_error = "";
-    
     AlignmentModel *alignmentModel =
         new AlignmentModel(reference, other, nullptr);
     rm->setAlignment(alignmentModel);
@@ -217,16 +383,16 @@
     connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
             this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
 
-    m_processModels[process] = alignmentModel;
+    m_pendingProcesses[process] = alignmentModel;
     process->start(program, args);
 
     bool success = process->waitForStarted();
 
     if (!success) {
-        cerr << "ERROR: Align::alignModelViaProgram: Program did not start"
-             << endl;
-        m_error = "Alignment program could not be started";
-        m_processModels.erase(process);
+        SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
+               << endl;
+        error = "Alignment program could not be started";
+        m_pendingProcesses.erase(process);
         rm->setAlignment(nullptr); // deletes alignmentModel as well
         delete process;
     }
@@ -237,17 +403,19 @@
 void
 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
 {
-    cerr << "Align::alignmentProgramFinished" << endl;
+    QMutexLocker locker (&m_mutex);
+    
+    SVCERR << "Align::alignmentProgramFinished" << endl;
     
     QProcess *process = qobject_cast<QProcess *>(sender());
 
-    if (m_processModels.find(process) == m_processModels.end()) {
-        cerr << "ERROR: Align::alignmentProgramFinished: Process " << process
-             << " not found in process model map!" << endl;
+    if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
+        SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
+               << " not found in process model map!" << endl;
         return;
     }
 
-    AlignmentModel *alignmentModel = m_processModels[process];
+    AlignmentModel *alignmentModel = m_pendingProcesses[process];
     
     if (exitCode == 0 && status == 0) {
 
@@ -269,10 +437,11 @@
 
         CSVFileReader reader(process, format, alignmentModel->getSampleRate());
         if (!reader.isOK()) {
-            cerr << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
-                 << endl;
-            m_error = QString("Failed to parse output of program: %1")
-                .arg(reader.getError());
+            SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
+                   << endl;
+            alignmentModel->setError
+                (QString("Failed to parse output of program: %1")
+                 .arg(reader.getError()));
             goto done;
         }
 
@@ -280,35 +449,38 @@
 
         SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput);
         if (!path) {
-            cerr << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
-                 << endl;
-            m_error = QString("Output of program did not produce sparse time-value model");
+            SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
+                   << endl;
+            alignmentModel->setError
+                ("Output of program did not produce sparse time-value model");
             goto done;
         }
 
-        if (path->getPoints().empty()) {
-            cerr << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
-                 << endl;
-            m_error = QString("Output of alignment program contained no mappings");
+        if (path->isEmpty()) {
+            SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
+                   << endl;
+            alignmentModel->setError
+                ("Output of alignment program contained no mappings");
             goto done;
         }
 
-        cerr << "Align::alignmentProgramFinished: Setting alignment path ("
-             << path->getPoints().size() << " point(s))" << endl;
-        
+        SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
+             << path->getEventCount() << " point(s))" << endl;
+
         alignmentModel->setPathFrom(path);
 
         emit alignmentComplete(alignmentModel);
         
     } else {
-        cerr << "ERROR: Align::alignmentProgramFinished: Aligner program "
-             << "failed: exit code " << exitCode << ", status " << status
-             << endl;
-        m_error = "Aligner process returned non-zero exit status";
+        SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
+               << "failed: exit code " << exitCode << ", status " << status
+               << endl;
+        alignmentModel->setError
+            ("Aligner process returned non-zero exit status");
     }
 
 done:
-    m_processModels.erase(process);
+    m_pendingProcesses.erase(process);
     delete process;
 }
 
--- a/framework/Align.h	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/Align.h	Fri May 17 09:46:22 2019 +0100
@@ -19,10 +19,13 @@
 #include <QString>
 #include <QObject>
 #include <QProcess>
+#include <QMutex>
 #include <set>
 
 class Model;
 class AlignmentModel;
+class SparseTimeValueModel;
+class AggregateWaveModel;
 class Document;
 
 class Align : public QObject
@@ -30,7 +33,7 @@
     Q_OBJECT
     
 public:
-    Align() : m_error("") { }
+    Align() { }
 
     /**
      * Align the "other" model to the reference, attaching an
@@ -38,6 +41,17 @@
      * configured in the user preferences (either a plugin transform
      * or an external process) and is done asynchronously. 
      *
+     * The return value indicates whether the alignment procedure
+     * started successfully. If it is true, then an AlignmentModel has
+     * been constructed and attached to the toAlign model, and you can
+     * query that model to discover the alignment progress, eventual
+     * outcome, and any error message generated during alignment. (The
+     * AlignmentModel is subsequently owned by the toAlign model.)
+     * Conversely if alignModel returns false, no AlignmentModel has
+     * been created, and the error return argument will contain an
+     * error report about whatever problem prevented this from
+     * happening.
+     *
      * A single Align object may carry out many simultanous alignment
      * calls -- you do not need to create a new Align object each
      * time, nor to wait for an alignment to be complete before
@@ -50,24 +64,25 @@
      */
     bool alignModel(Document *doc,
                     Model *reference,
-                    Model *other); // via user preference
+                    Model *toAlign,
+                    QString &error);
     
     bool alignModelViaTransform(Document *doc,
                                 Model *reference,
-                                Model *other);
+                                Model *toAlign,
+                                QString &error);
 
     bool alignModelViaProgram(Document *doc,
                               Model *reference,
-                              Model *other,
-                              QString program);
+                              Model *toAlign,
+                              QString program,
+                              QString &error);
 
     /**
      * Return true if the alignment facility is available (relevant
      * plugin installed, etc).
      */
     static bool canAlign();
-    
-    QString getError() const { return m_error; }
 
 signals:
     /**
@@ -79,13 +94,30 @@
 
 private slots:
     void alignmentCompletionChanged();
+    void tuningDifferenceCompletionChanged();
     void alignmentProgramFinished(int, QProcess::ExitStatus);
     
 private:
     static QString getAlignmentTransformName();
-    
-    QString m_error;
-    std::map<QProcess *, AlignmentModel *> m_processModels;
+    static QString getTuningDifferenceTransformName();
+
+    bool beginTransformDrivenAlignment(AggregateWaveModel *,
+                                       AlignmentModel *,
+                                       float tuningFrequency = 0.f);
+
+    QMutex m_mutex;
+
+    struct TuningDiffRec {
+        AggregateWaveModel *input;
+        AlignmentModel *alignment;
+        SparseTimeValueModel *preparatory;
+    };
+
+    // tuning-difference output model -> data needed for subsequent alignment
+    std::map<SparseTimeValueModel *, TuningDiffRec> m_pendingTuningDiffs;
+
+    // external alignment subprocess -> model into which to stuff the results
+    std::map<QProcess *, AlignmentModel *> m_pendingProcesses;
 };
 
 #endif
--- a/framework/Document.cpp	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/Document.cpp	Fri May 17 09:46:22 2019 +0100
@@ -21,7 +21,6 @@
 #include "data/model/WritableWaveFileModel.h"
 #include "data/model/DenseThreeDimensionalModel.h"
 #include "data/model/DenseTimeValueModel.h"
-#include "data/model/FlexiNoteModel.h"
 #include "data/model/AggregateWaveModel.h"
 
 #include "layer/Layer.h"
@@ -75,7 +74,7 @@
     //the document, be nice to fix that
 
 #ifdef DEBUG_DOCUMENT
-    cerr << "\n\nDocument::~Document: about to clear command history" << endl;
+    SVDEBUG << "\n\nDocument::~Document: about to clear command history" << endl;
 #endif
     CommandHistory::getInstance()->clear();
     
@@ -91,8 +90,8 @@
                   << m_models.size() << " model(s) still remain -- "
                   << "should have been garbage collected when deleting layers"
                   << endl;
-        while (!m_models.empty()) {
-            Model *model = m_models.begin()->first;
+        for (ModelRecord &rec: m_models) {
+            Model *model = rec.model;
             if (model == m_mainModel) {
                 // just in case!
                 SVDEBUG << "Document::~Document: WARNING: Main model is also"
@@ -102,8 +101,8 @@
                 emit modelAboutToBeDeleted(model);
                 delete model;
             }
-            m_models.erase(m_models.begin());
         }
+        m_models.clear();
     }
 
 #ifdef DEBUG_DOCUMENT
@@ -127,7 +126,7 @@
 
     newLayer->setObjectName(getUniqueLayerName(newLayer->objectName()));
 
-    m_layers.insert(newLayer);
+    m_layers.push_back(newLayer);
 
 #ifdef DEBUG_DOCUMENT
     SVDEBUG << "Document::createLayer: Added layer of type " << type
@@ -155,7 +154,7 @@
         LayerFactory::getInstance()->getValidLayerTypes(model);
 
     if (types.empty()) {
-        cerr << "WARNING: Document::importLayer: no valid display layer for model" << endl;
+        SVCERR << "WARNING: Document::importLayer: no valid display layer for model" << endl;
         return nullptr;
     }
 
@@ -173,7 +172,7 @@
     //!!! and all channels
     setChannel(newLayer, -1);
 
-    m_layers.insert(newLayer);
+    m_layers.push_back(newLayer);
 
 #ifdef DEBUG_DOCUMENT
     SVDEBUG << "Document::createImportedLayer: Added layer of type " << type
@@ -277,7 +276,7 @@
 
     void
     moreModelsAvailable(vector<Model *> models) override {
-        std::cerr << "AdditionalModelConverter::moreModelsAvailable: " << models.size() << " model(s)" << std::endl;
+        SVDEBUG << "AdditionalModelConverter::moreModelsAvailable: " << models.size() << " model(s)" << endl;
         // We can't automatically regenerate the additional models on
         // reload -- we should delete them instead
         QStringList names;
@@ -293,7 +292,7 @@
 
     void
     noMoreModelsAvailable() override {
-        std::cerr << "AdditionalModelConverter::noMoreModelsAvailable" << std::endl;
+        SVDEBUG << "AdditionalModelConverter::noMoreModelsAvailable" << endl;
         m_handler->layersCreated(this, m_primary, vector<Layer *>());
         delete this;
     }
@@ -370,12 +369,9 @@
             LayerFactory::getInstance()->getValidLayerTypes(newModel);
 
         if (types.empty()) {
-            cerr << "WARNING: Document::createLayerForTransformer: no valid display layer for output of transform " << names[i] << endl;
+            SVCERR << "WARNING: Document::createLayerForTransformer: no valid display layer for output of transform " << names[i] << endl;
             //!!! inadequate cleanup:
-            newModel->aboutToDelete();
-            emit modelAboutToBeDeleted(newModel);
-            m_models.erase(newModel);
-            delete newModel;
+            deleteModelFromList(newModel);
             return vector<Layer *>();
         }
 
@@ -433,62 +429,64 @@
     // delete any of the models.
 
 #ifdef DEBUG_DOCUMENT
-    cerr << "Document::setMainModel: Have "
+    SVDEBUG << "Document::setMainModel: Have "
               << m_layers.size() << " layers" << endl;
-    cerr << "Models now: ";
-    for (ModelMap::const_iterator i = m_models.begin(); i != m_models.end(); ++i) {
-        cerr << i->first << " ";
-    } 
-    cerr << endl;
-    cerr << "Old main model: " << oldMainModel << endl;
+    SVDEBUG << "Models now: ";
+    for (const auto &r: m_models) {
+        SVDEBUG << r.model << " ";
+    }
+    SVDEBUG << endl;
+    SVDEBUG << "Old main model: " << oldMainModel << endl;
 #endif
 
-    for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+    for (Layer *layer: m_layers) {
 
-        Layer *layer = *i;
         Model *model = layer->getModel();
 
 #ifdef DEBUG_DOCUMENT
-        cerr << "Document::setMainModel: inspecting model "
-                  << (model ? model->objectName(): "(null)") << " in layer "
-                  << layer->objectName() << endl;
+        SVDEBUG << "Document::setMainModel: inspecting model "
+                << (model ? model->objectName(): "(null)") << " in layer "
+                << layer->objectName() << endl;
 #endif
 
         if (model == oldMainModel) {
 #ifdef DEBUG_DOCUMENT
-            cerr << "... it uses the old main model, replacing" << endl;
+            SVDEBUG << "... it uses the old main model, replacing" << endl;
 #endif
             LayerFactory::getInstance()->setModel(layer, m_mainModel);
             continue;
         }
 
         if (!model) {
-            cerr << "WARNING: Document::setMainModel: Null model in layer "
-                      << layer << endl;
+            SVCERR << "WARNING: Document::setMainModel: Null model in layer "
+                   << layer << endl;
             // get rid of this hideous degenerate
             obsoleteLayers.push_back(layer);
             continue;
         }
 
-        if (m_models.find(model) == m_models.end()) {
-            cerr << "WARNING: Document::setMainModel: Unknown model "
-                      << model << " in layer " << layer << endl;
+        auto mitr = findModelInList(model);
+        
+        if (mitr == m_models.end()) {
+            SVCERR << "WARNING: Document::setMainModel: Unknown model "
+                   << model << " in layer " << layer << endl;
             // and this one
             obsoleteLayers.push_back(layer);
             continue;
         }
-            
-        if (m_models[model].source &&
-            (m_models[model].source == oldMainModel)) {
+
+        ModelRecord record = *mitr;
+        
+        if (record.source && (record.source == oldMainModel)) {
 
 #ifdef DEBUG_DOCUMENT
-            cerr << "... it uses a model derived from the old main model, regenerating" << endl;
+            SVDEBUG << "... it uses a model derived from the old main model, regenerating" << endl;
 #endif
 
             // This model was derived from the previous main
             // model: regenerate it.
             
-            const Transform &transform = m_models[model].transform;
+            const Transform &transform = record.transform;
             QString transformId = transform.getIdentifier();
             
             //!!! We have a problem here if the number of channels in
@@ -498,12 +496,12 @@
             Model *replacementModel =
                 addDerivedModel(transform,
                                 ModelTransformer::Input
-                                (m_mainModel, m_models[model].channel),
+                                (m_mainModel, record.channel),
                                 message);
             
             if (!replacementModel) {
-                cerr << "WARNING: Document::setMainModel: Failed to regenerate model for transform \""
-                          << transformId << "\"" << " in layer " << layer << endl;
+                SVCERR << "WARNING: Document::setMainModel: Failed to regenerate model for transform \""
+                       << transformId << "\"" << " in layer " << layer << endl;
                 if (failedTransformers.find(transformId)
                     == failedTransformers.end()) {
                     emit modelRegenerationFailed(layer->objectName(),
@@ -519,19 +517,19 @@
                                                   message);
                 }
 #ifdef DEBUG_DOCUMENT
-                cerr << "Replacing model " << model << " (type "
-                          << typeid(*model).name() << ") with model "
-                          << replacementModel << " (type "
-                          << typeid(*replacementModel).name() << ") in layer "
-                          << layer << " (name " << layer->objectName() << ")"
-                          << endl;
+                SVDEBUG << "Replacing model " << model << " (type "
+                        << typeid(*model).name() << ") with model "
+                        << replacementModel << " (type "
+                        << typeid(*replacementModel).name() << ") in layer "
+                        << layer << " (name " << layer->objectName() << ")"
+                        << endl;
 
                 RangeSummarisableTimeValueModel *rm =
                     dynamic_cast<RangeSummarisableTimeValueModel *>(replacementModel);
                 if (rm) {
-                    cerr << "new model has " << rm->getChannelCount() << " channels " << endl;
+                    SVDEBUG << "new model has " << rm->getChannelCount() << " channels " << endl;
                 } else {
-                    cerr << "new model " << replacementModel << " is not a RangeSummarisableTimeValueModel!" << endl;
+                    SVDEBUG << "new model " << replacementModel << " is not a RangeSummarisableTimeValueModel!" << endl;
                 }
 #endif
                 setModel(layer, replacementModel);
@@ -543,18 +541,19 @@
         deleteLayer(obsoleteLayers[k], true);
     }
 
-    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
-        if (i->second.additional) {
-            Model *m = i->first;
-            m->aboutToDelete();
-            emit modelAboutToBeDeleted(m);
-            delete m;
+    std::set<Model *> additionalModels;
+    for (const auto &rec : m_models) {
+        if (rec.additional) {
+            additionalModels.insert(rec.model);
         }
     }
+    for (Model *a: additionalModels) {
+        deleteModelFromList(a);
+    }
 
-    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+    for (const auto &rec : m_models) {
 
-        Model *m = i->first;
+        Model *m = rec.model;
 
 #ifdef DEBUG_DOCUMENT
         SVDEBUG << "considering alignment for model " << m << " (name \""
@@ -580,6 +579,8 @@
     if (m_autoAlignment) {
         SVDEBUG << "Document::setMainModel: auto-alignment is on, aligning model if possible" << endl;
         alignModel(m_mainModel);
+    } else {
+        SVDEBUG << "Document::setMainModel: auto-alignment is off" << endl;
     }
 
     emit mainModelChanged(m_mainModel);
@@ -592,21 +593,22 @@
                                  const ModelTransformer::Input &input,
                                  Model *outputModelToAdd)
 {
-    if (m_models.find(outputModelToAdd) != m_models.end()) {
-        cerr << "WARNING: Document::addAlreadyDerivedModel: Model already added"
+    if (findModelInList(outputModelToAdd) != m_models.end()) {
+        SVCERR << "WARNING: Document::addAlreadyDerivedModel: Model already added"
                   << endl;
         return;
     }
 
 #ifdef DEBUG_DOCUMENT
     if (input.getModel()) {
-        cerr << "Document::addAlreadyDerivedModel: source is " << input.getModel() << " \"" << input.getModel()->objectName() << "\"" << endl;
+        SVDEBUG << "Document::addAlreadyDerivedModel: source is " << input.getModel() << " \"" << input.getModel()->objectName() << "\"" << endl;
     } else {
-        cerr << "Document::addAlreadyDerivedModel: source is " << input.getModel() << endl;
+        SVDEBUG << "Document::addAlreadyDerivedModel: source is " << input.getModel() << endl;
     }
 #endif
 
     ModelRecord rec;
+    rec.model = outputModelToAdd;
     rec.source = input.getModel();
     rec.channel = input.getChannel();
     rec.transform = transform;
@@ -615,15 +617,15 @@
 
     outputModelToAdd->setSourceModel(input.getModel());
 
-    m_models[outputModelToAdd] = rec;
+    m_models.push_back(rec);
 
 #ifdef DEBUG_DOCUMENT
-    cerr << "Document::addAlreadyDerivedModel: Added model " << outputModelToAdd << endl;
-    cerr << "Models now: ";
-    for (ModelMap::const_iterator i = m_models.begin(); i != m_models.end(); ++i) {
-        cerr << i->first << " ";
+    SVDEBUG << "Document::addAlreadyDerivedModel: Added model " << outputModelToAdd << endl;
+    SVDEBUG << "Models now: ";
+    for (const auto &rec : m_models) {
+        SVDEBUG << rec.model << " ";
     } 
-    cerr << endl;
+    SVDEBUG << endl;
 #endif
 
     emit modelAdded(outputModelToAdd);
@@ -633,27 +635,28 @@
 void
 Document::addImportedModel(Model *model)
 {
-    if (m_models.find(model) != m_models.end()) {
-        cerr << "WARNING: Document::addImportedModel: Model already added"
+    if (findModelInList(model) != m_models.end()) {
+        SVCERR << "WARNING: Document::addImportedModel: Model already added"
                   << endl;
         return;
     }
 
     ModelRecord rec;
+    rec.model = model;
     rec.source = nullptr;
     rec.channel = 0;
     rec.refcount = 0;
     rec.additional = false;
 
-    m_models[model] = rec;
+    m_models.push_back(rec);
 
 #ifdef DEBUG_DOCUMENT
     SVDEBUG << "Document::addImportedModel: Added model " << model << endl;
-    cerr << "Models now: ";
-    for (ModelMap::const_iterator i = m_models.begin(); i != m_models.end(); ++i) {
-        cerr << i->first << " ";
+    SVDEBUG << "Models now: ";
+    for (const auto &rec : m_models) {
+        SVDEBUG << rec.model << " ";
     } 
-    cerr << endl;
+    SVDEBUG << endl;
 #endif
 
     if (m_autoAlignment) {
@@ -669,27 +672,28 @@
 void
 Document::addAdditionalModel(Model *model)
 {
-    if (m_models.find(model) != m_models.end()) {
-        cerr << "WARNING: Document::addAdditionalModel: Model already added"
+    if (findModelInList(model) != m_models.end()) {
+        SVCERR << "WARNING: Document::addAdditionalModel: Model already added"
                   << endl;
         return;
     }
 
     ModelRecord rec;
+    rec.model = model;
     rec.source = nullptr;
     rec.channel = 0;
     rec.refcount = 0;
     rec.additional = true;
 
-    m_models[model] = rec;
+    m_models.push_back(rec);
 
 #ifdef DEBUG_DOCUMENT
     SVDEBUG << "Document::addAdditionalModel: Added model " << model << endl;
-    cerr << "Models now: ";
-    for (ModelMap::const_iterator i = m_models.begin(); i != m_models.end(); ++i) {
-        cerr << i->first << " ";
+    SVDEBUG << "Models now: ";
+    for (const auto &rec : m_models) {
+        SVDEBUG << rec.model << " ";
     } 
-    cerr << endl;
+    SVDEBUG << endl;
 #endif
 
     if (m_autoAlignment) {
@@ -706,6 +710,7 @@
     connect(model, SIGNAL(modelInvalidated()),
             this, SLOT(aggregateModelInvalidated()));
     m_aggregateModels.insert(model);
+    SVDEBUG << "Document::addAggregateModel(" << model << ")" << endl;
 }
 
 void
@@ -713,6 +718,7 @@
 {
     QObject *s = sender();
     AggregateWaveModel *aggregate = qobject_cast<AggregateWaveModel *>(s);
+    SVDEBUG << "Document::aggregateModelInvalidated(" << aggregate << ")" << endl;
     if (aggregate) releaseModel(aggregate);
 }
 
@@ -721,12 +727,12 @@
                           const ModelTransformer::Input &input,
                           QString &message)
 {
-    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
-        if (i->second.transform == transform &&
-            i->second.source == input.getModel() && 
-            i->second.channel == input.getChannel()) {
-            std::cerr << "derived model taken from map " << std::endl;
-            return i->first;
+    for (auto &rec : m_models) {
+        if (rec.transform == transform &&
+            rec.source == input.getModel() && 
+            rec.channel == input.getChannel()) {
+            SVDEBUG << "derived model taken from map " << endl;
+            return rec.model;
         }
     }
 
@@ -771,7 +777,7 @@
              .getPluginVersion());
 
         if (!model) {
-            cerr << "WARNING: Document::addDerivedModel: no output model for transform " << applied.getIdentifier() << endl;
+            SVCERR << "WARNING: Document::addDerivedModel: no output model for transform " << applied.getIdentifier() << endl;
         } else {
             addAlreadyDerivedModel(applied, input, model);
         }
@@ -799,12 +805,17 @@
 
     bool toDelete = false;
 
-    if (m_models.find(model) != m_models.end()) {
-        if (m_models[model].refcount == 0) {
+    ModelList::iterator mitr = findModelInList(model);
+    
+    if (mitr != m_models.end()) {
+        if (mitr->refcount == 0) {
             SVCERR << "WARNING: Document::releaseModel: model " << model
                    << " reference count is zero already!" << endl;
         } else {
-            if (--m_models[model].refcount == 0) {
+#ifdef DEBUG_DOCUMENT
+            SVDEBUG << "Lowering refcount from " << mitr->refcount << endl;
+#endif
+            if (--mitr->refcount == 0) {
                 toDelete = true;
             }
         }
@@ -823,10 +834,10 @@
 
         int sourceCount = 0;
 
-        for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
-            if (i->second.source == model) {
+        for (auto &rec: m_models) {
+            if (rec.source == model) {
                 ++sourceCount;
-                i->second.source = nullptr;
+                rec.source = nullptr;
             }
         }
 
@@ -837,20 +848,16 @@
                     << "their source fields appropriately" << endl;
         }
 
-        model->aboutToDelete();
-        emit modelAboutToBeDeleted(model);
-        m_models.erase(model);
+        deleteModelFromList(model);
 
 #ifdef DEBUG_DOCUMENT
         SVDEBUG << "Document::releaseModel: Deleted model " << model << endl;
-        cerr << "Models now: ";
-        for (ModelMap::const_iterator i = m_models.begin(); i != m_models.end(); ++i) {
-            cerr << i->first << " ";
+        SVDEBUG << "Models now: ";
+        for (const auto &r: m_models) {
+            SVDEBUG << r.model << " ";
         } 
-        cerr << endl;
+        SVDEBUG << endl;
 #endif
-
-        delete model;
     }
 }
 
@@ -860,17 +867,13 @@
     if (m_layerViewMap.find(layer) != m_layerViewMap.end() &&
         m_layerViewMap[layer].size() > 0) {
 
-        cerr << "WARNING: Document::deleteLayer: Layer "
-                  << layer << " [" << layer->objectName() << "]"
-                  << " is still used in " << m_layerViewMap[layer].size()
-                  << " views!" << endl;
-
         if (force) {
 
-#ifdef DEBUG_DOCUMENT
-            cerr << "(force flag set -- deleting from all views)" << endl;
-#endif
-
+            SVDEBUG << "NOTE: Document::deleteLayer: Layer "
+                    << layer << " [" << layer->objectName() << "]"
+                    << " is still used in " << m_layerViewMap[layer].size()
+                    << " views. Force flag set, so removing from them" << endl;
+            
             for (std::set<View *>::iterator j = m_layerViewMap[layer].begin();
                  j != m_layerViewMap[layer].end(); ++j) {
                 // don't use removeLayerFromView, as it issues a command
@@ -881,11 +884,25 @@
             m_layerViewMap.erase(layer);
 
         } else {
+
+            SVCERR << "WARNING: Document::deleteLayer: Layer "
+                   << layer << " [" << layer->objectName() << "]"
+                   << " is still used in " << m_layerViewMap[layer].size()
+                   << " views! Force flag is not set, so not deleting" << endl;
+            
             return;
         }
     }
 
-    if (m_layers.find(layer) == m_layers.end()) {
+    bool found = false;
+    for (auto itr = m_layers.begin(); itr != m_layers.end(); ++itr) {
+        if (*itr == layer) {
+            found = true;
+            m_layers.erase(itr);
+            break;
+        }
+    }
+    if (!found) {
         SVDEBUG << "Document::deleteLayer: Layer "
                   << layer << " (typeid " << typeid(layer).name() <<
                   ") does not exist, or has already been deleted "
@@ -893,8 +910,6 @@
         return;
     }
 
-    m_layers.erase(layer);
-
 #ifdef DEBUG_DOCUMENT
     SVDEBUG << "Document::deleteLayer: Removing (and about to release model), now have "
               << m_layers.size() << " layers" << endl;
@@ -911,8 +926,8 @@
 {
     if (model && 
         model != m_mainModel &&
-        m_models.find(model) == m_models.end()) {
-        cerr << "ERROR: Document::setModel: Layer " << layer
+        findModelInList(model) == m_models.end()) {
+        SVCERR << "ERROR: Document::setModel: Layer " << layer
                   << " (\"" << layer->objectName()
                   << "\") wants to use unregistered model " << model
                   << ": register the layer's model before setting it!"
@@ -932,7 +947,10 @@
     }
 
     if (model && model != m_mainModel) {
-        m_models[model].refcount ++;
+        ModelList::iterator mitr = findModelInList(model);
+        if (mitr != m_models.end()) {
+            mitr->refcount ++;
+        }
     }
 
     if (model && previousModel) {
@@ -941,7 +959,6 @@
     }
 
     LayerFactory::getInstance()->setModel(layer, model);
-        // std::cerr << "layer type: " << LayerFactory::getInstance()->getLayerTypeName(LayerFactory::getInstance()->getLayerType(layer)) << std::endl;
 
     if (previousModel) {
         releaseModel(previousModel);
@@ -961,13 +978,14 @@
     if (!model) {
 #ifdef DEBUG_DOCUMENT
         SVDEBUG << "Document::addLayerToView: Layer (\""
-                  << layer->objectName()                  << "\") with no model being added to view: "
+                  << layer->objectName()
+                << "\") with no model being added to view: "
                   << "normally you want to set the model first" << endl;
 #endif
     } else {
         if (model != m_mainModel &&
-            m_models.find(model) == m_models.end()) {
-            cerr << "ERROR: Document::addLayerToView: Layer " << layer
+            findModelInList(model) == m_models.end()) {
+            SVCERR << "ERROR: Document::addLayerToView: Layer " << layer
                       << " has unregistered model " << model
                       << " -- register the layer's model before adding the layer!" << endl;
             return;
@@ -993,7 +1011,7 @@
 
     if (m_layerViewMap[layer].find(view) !=
         m_layerViewMap[layer].end()) {
-        cerr << "WARNING: Document::addToLayerViewMap:"
+        SVCERR << "WARNING: Document::addToLayerViewMap:"
                   << " Layer " << layer << " -> view " << view << " already in"
                   << " layer view map -- internal inconsistency" << endl;
     }
@@ -1008,7 +1026,7 @@
 {
     if (m_layerViewMap[layer].find(view) ==
         m_layerViewMap[layer].end()) {
-        cerr << "WARNING: Document::removeFromLayerViewMap:"
+        SVCERR << "WARNING: Document::removeFromLayerViewMap:"
                   << " Layer " << layer << " -> view " << view << " not in"
                   << " layer view map -- internal inconsistency" << endl;
     }
@@ -1032,7 +1050,7 @@
         
         bool duplicate = false;
 
-        for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) {
+        for (auto i = m_layers.begin(); i != m_layers.end(); ++i) {
             if ((*i)->objectName() == adjusted) {
                 duplicate = true;
                 break;
@@ -1054,9 +1072,9 @@
 
     //!!! This will pick up all models, including those that aren't visible...
 
-    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
+    for (ModelRecord &rec: m_models) {
 
-        Model *model = i->first;
+        Model *model = rec.model;
         if (!model || model == m_mainModel) continue;
         DenseTimeValueModel *dtvm = dynamic_cast<DenseTimeValueModel *>(model);
         
@@ -1072,7 +1090,10 @@
 Document::isKnownModel(const Model *model) const
 {
     if (model == m_mainModel) return true;
-    return (m_models.find(const_cast<Model *>(model)) != m_models.end());
+    for (const ModelRecord &rec: m_models) {
+        if (rec.model == model) return true;
+    }
+    return false;
 }
 
 bool
@@ -1082,25 +1103,37 @@
 }
 
 void
-Document::alignModel(Model *model)
+Document::alignModel(Model *model, bool forceRecalculate)
 {
-    SVDEBUG << "Document::alignModel(" << model << ")" << endl;
-
-    if (!m_mainModel) {
-        SVDEBUG << "(no main model to align to)" << endl;
-        return;
-    }
+    SVDEBUG << "Document::alignModel(" << model << ", " << forceRecalculate
+            << ") (main model is " << m_mainModel << ")" << endl;
 
     RangeSummarisableTimeValueModel *rm = 
         dynamic_cast<RangeSummarisableTimeValueModel *>(model);
     if (!rm) {
-        SVDEBUG << "(main model is not alignable-to)" << endl;
+        SVDEBUG << "(model " << rm << " is not an alignable sort)" << endl;
+        return;
+    }
+
+    if (!m_mainModel) {
+        SVDEBUG << "(no main model to align to)" << endl;
+        if (forceRecalculate && rm->getAlignment()) {
+            SVDEBUG << "(but model is aligned, and forceRecalculate is true, "
+                    << "so resetting alignment to nil)" << endl;
+            rm->setAlignment(nullptr);
+        }
         return;
     }
 
     if (rm->getAlignmentReference() == m_mainModel) {
-        SVDEBUG << "(model " << rm << " is already aligned to main model " << m_mainModel << ")" << endl;
-        return;
+        SVDEBUG << "(model " << rm << " is already aligned to main model "
+                << m_mainModel << ")" << endl;
+        if (!forceRecalculate) {
+            return;
+        } else {
+            SVDEBUG << "(but forceRecalculate is true, so realigning anyway)"
+                    << endl;
+        }
     }
     
     if (model == m_mainModel) {
@@ -1108,22 +1141,40 @@
         // it possible to distinguish between the reference and any
         // unaligned model just by looking at the model itself,
         // without also knowing what the main model is
-        SVDEBUG << "Document::alignModel(" << model << "): is main model, setting appropriately" << endl;
+        SVDEBUG << "Document::alignModel(" << model
+                << "): is main model, setting alignment to itself" << endl;
         rm->setAlignment(new AlignmentModel(model, model, nullptr));
         return;
     }
 
-    if (!m_align->alignModel(this, m_mainModel, rm)) {
-        SVCERR << "Alignment failed: " << m_align->getError() << endl;
-        emit alignmentFailed(m_align->getError());
+    SVDEBUG << "Document::alignModel: aligning..." << endl;
+    if (rm->getAlignmentReference() != nullptr) {
+        SVDEBUG << "(Note: model " << rm << " is currently aligned to model "
+                << rm->getAlignmentReference() << "; this will replace that)"
+                << endl;
+    }
+
+    QString err;
+    if (!m_align->alignModel(this, m_mainModel, rm, err)) {
+        SVCERR << "Alignment failed: " << err << endl;
+        emit alignmentFailed(err);
     }
 }
 
 void
 Document::alignModels()
 {
-    for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) {
-        alignModel(i->first);
+    for (const ModelRecord &rec: m_models) {
+        alignModel(rec.model);
+    }
+    alignModel(m_mainModel);
+}
+
+void
+Document::realignModels()
+{
+    for (const ModelRecord &rec: m_models) {
+        alignModel(rec.model, true);
     }
     alignModel(m_mainModel);
 }
@@ -1275,6 +1326,10 @@
 
     if (m_mainModel) {
 
+#ifdef DEBUG_DOCUMENT
+        SVDEBUG << "Document::toXml: writing main model" << endl;
+#endif
+        
         if (asTemplate) {
             writePlaceholderMainModel(out, indent + "  ");
         } else {
@@ -1287,8 +1342,12 @@
             playParameters->toXml
                 (out, indent + "  ",
                  QString("model=\"%1\"")
-                 .arg(XmlExportable::getObjectExportId(m_mainModel)));
+                 .arg(m_mainModel->getExportId()));
         }
+    } else {
+#ifdef DEBUG_DOCUMENT
+        SVDEBUG << "Document::toXml: have no main model to write" << endl;
+#endif
     }
 
     // Models that are not used in a layer that is in a view should
@@ -1326,16 +1385,22 @@
     for (std::set<Model *>::iterator i = m_aggregateModels.begin();
          i != m_aggregateModels.end(); ++i) {
 
-        SVDEBUG << "checking aggregate model " << *i << endl;
+#ifdef DEBUG_DOCUMENT
+        SVDEBUG << "Document::toXml: checking aggregate model " << *i << endl;
+#endif
         
         AggregateWaveModel *aggregate = qobject_cast<AggregateWaveModel *>(*i);
         if (!aggregate) continue; 
         if (used.find(aggregate) == used.end()) {
+#ifdef DEBUG_DOCUMENT
             SVDEBUG << "(unused, skipping)" << endl;
+#endif
             continue;
         }
 
+#ifdef DEBUG_DOCUMENT
         SVDEBUG << "(used, writing)" << endl;
+#endif
 
         aggregate->toXml(out, indent + "  ");
     }
@@ -1352,14 +1417,19 @@
     const int nonDerivedPass = 0, derivedPass = 1;
     for (int pass = nonDerivedPass; pass <= derivedPass; ++pass) {
     
-        for (ModelMap::const_iterator i = m_models.begin();
-             i != m_models.end(); ++i) {
+        for (const ModelRecord &rec: m_models) {
 
-            Model *model = i->first;
-            const ModelRecord &rec = i->second;
+            Model *model = rec.model;
 
             if (used.find(model) == used.end()) continue;
         
+#ifdef DEBUG_DOCUMENT
+            SVDEBUG << "Document::toXml: looking at model " << model
+                    << " (" << model->getTypeName() << ", \""
+                    << model->objectName() << "\") [pass = "
+                    << pass << "]" << endl;
+#endif
+            
             // We need an intelligent way to determine which models
             // need to be streamed (i.e. have been edited, or are
             // small) and which should not be (i.e. remain as
@@ -1418,7 +1488,7 @@
                 playParameters->toXml
                     (out, indent + "  ",
                      QString("model=\"%1\"")
-                     .arg(XmlExportable::getObjectExportId(model)));
+                     .arg(model->getExportId()));
             }
         }
     }
@@ -1439,9 +1509,7 @@
         alignment->toXml(out, indent + "  ");
     }
 
-    for (LayerSet::const_iterator i = m_layers.begin();
-         i != m_layers.end(); ++i) {
-
+    for (auto i = m_layers.begin(); i != m_layers.end(); ++i) {
         (*i)->toXml(out, indent + "  ");
     }
 
@@ -1453,7 +1521,7 @@
 {
     out << indent;
     out << QString("<model id=\"%1\" name=\"placeholder\" sampleRate=\"%2\" type=\"wavefile\" file=\":samples/silent.wav\" mainModel=\"true\"/>\n")
-        .arg(getObjectExportId(m_mainModel))
+        .arg(m_mainModel->getExportId())
         .arg(m_mainModel->getSampleRate());
 }
 
@@ -1492,8 +1560,8 @@
     //    out << indent
     //        << QString("<derivation type=\"transform\" source=\"%1\" "
     //                   "model=\"%2\" channel=\"%3\">\n")
-    //        .arg(XmlExportable::getObjectExportId(rec.source))
-    //        .arg(XmlExportable::getObjectExportId(targetModel))
+    //        .arg(rec.source->getExportId())
+    //        .arg(targetModel->getExportId())
     //        .arg(rec.channel);
     //
     //    transform.toXml(out, indent + "  ");
@@ -1517,8 +1585,8 @@
                    "model=\"%2\" channel=\"%3\" domain=\"%4\" "
                    "stepSize=\"%5\" blockSize=\"%6\" %7windowType=\"%8\" "
                    "transform=\"%9\">\n")
-        .arg(XmlExportable::getObjectExportId(rec.source))
-        .arg(XmlExportable::getObjectExportId(targetModel))
+        .arg(rec.source->getExportId())
+        .arg(targetModel->getExportId())
         .arg(rec.channel)
         .arg(TransformFactory::getInstance()->getTransformInputDomain
              (transform.getIdentifier()))
--- a/framework/Document.h	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/Document.h	Fri May 17 09:46:22 2019 +0100
@@ -294,6 +294,12 @@
     void alignModels();
 
     /**
+     * Re-generate alignments for all appropriate models against the
+     * main model.  Existing alignments will be re-calculated.
+     */
+    void realignModels();
+
+    /**
      * Return true if any external files (most obviously audio) failed
      * to be found on load, so that the document is incomplete
      * compared to its saved description.
@@ -338,11 +344,11 @@
 
     /**
      * If model is suitable for alignment, align it against the main
-     * model and store the alignment in the model.  (If the model has
-     * an alignment already for the current main model, leave it
-     * unchanged.)
+     * model and store the alignment in the model. If the model has an
+     * alignment already for the current main model, leave it
+     * unchanged unless forceRecalculate is true.
      */
-    void alignModel(Model *);
+    void alignModel(Model *, bool forceRecalculate = false);
 
     /*
      * Every model that is in use by a layer in the document must be
@@ -370,6 +376,7 @@
         // be confusing to have Input objects hanging around with NULL
         // models in them.
 
+        Model *model;
         const Model *source;
         int channel;
         Transform transform;
@@ -379,8 +386,41 @@
         int refcount;
     };
 
-    typedef std::map<Model *, ModelRecord> ModelMap;
-    ModelMap m_models;
+    // This used to be a map<Model *, ModelRecord>, but a vector
+    // ensures that models are consistently recorded in order of
+    // creation rather than at the whim of heap allocation, and that's
+    // useful for automated testing. We don't expect ever to have so
+    // many models that there is any significant overhead there.
+    typedef std::vector<ModelRecord> ModelList;
+    ModelList m_models;
+
+    ModelList::iterator findModelInList(Model *m) {
+        for (ModelList::iterator i = m_models.begin();
+             i != m_models.end(); ++i) {
+            if (i->model == m) {
+                return i;
+            }
+        }
+        return m_models.end();
+    }
+    
+    void deleteModelFromList(Model *m) {
+        ModelList keep;
+        bool found = false;
+        for (const ModelRecord &rec: m_models) {
+            if (rec.model == m) {
+                found = true;
+                m->aboutToDelete();
+                emit modelAboutToBeDeleted(m);
+            } else {
+                keep.push_back(rec);
+            }
+        }
+        m_models = keep;
+        if (found) {
+            delete m;
+        }
+    }
 
     /**
      * Add an extra derived model (returned at the end of processing a
@@ -447,8 +487,8 @@
      * And these are the layers.  We also control the lifespans of
      * these (usually through the commands used to add and remove them).
      */
-    typedef std::set<Layer *> LayerSet;
-    LayerSet m_layers;
+    typedef std::vector<Layer *> LayerList;
+    LayerList m_layers;
 
     bool m_autoAlignment;
     Align *m_align;
--- a/framework/MainWindowBase.cpp	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/MainWindowBase.cpp	Fri May 17 09:46:22 2019 +0100
@@ -22,7 +22,6 @@
 #include "data/model/WritableWaveFileModel.h"
 #include "data/model/SparseOneDimensionalModel.h"
 #include "data/model/NoteModel.h"
-#include "data/model/FlexiNoteModel.h"
 #include "data/model/Labeller.h"
 #include "data/model/TabularModel.h"
 #include "view/ViewManager.h"
@@ -55,10 +54,12 @@
 #include "data/fileio/PlaylistFileReader.h"
 #include "data/fileio/WavFileWriter.h"
 #include "data/fileio/MIDIFileWriter.h"
+#include "data/fileio/CSVFileWriter.h"
 #include "data/fileio/BZipFileDevice.h"
 #include "data/fileio/FileSource.h"
 #include "data/fileio/AudioFileReaderFactory.h"
 #include "rdf/RDFImporter.h"
+#include "rdf/RDFExporter.h"
 
 #include "base/RecentFiles.h"
 
@@ -72,6 +73,7 @@
 
 #include "data/osc/OSCQueue.h"
 #include "data/midi/MIDIInput.h"
+#include "OSCScript.h"
 
 #include "system/System.h"
 
@@ -149,6 +151,7 @@
     m_audioIO(nullptr),
     m_oscQueue(nullptr),
     m_oscQueueStarter(nullptr),
+    m_oscScript(nullptr),
     m_midiInput(nullptr),
     m_recentFiles("RecentFiles", 20),
     m_recentTransforms("RecentTransforms", 20),
@@ -328,6 +331,17 @@
     delete m_viewManager;
     delete m_midiInput;
 
+    if (m_oscScript) {
+        disconnect(m_oscScript, nullptr, nullptr, nullptr);
+        m_oscScript->abandon();
+        m_oscScript->wait(1000);
+        if (m_oscScript->isRunning()) {
+            m_oscScript->terminate();
+            m_oscScript->wait(1000);
+        }
+        delete m_oscScript;
+    }
+
     if (m_oscQueueStarter) {
         disconnect(m_oscQueueStarter, nullptr, nullptr, nullptr);
         m_oscQueueStarter->wait(1000);
@@ -501,9 +515,9 @@
 }
 
 void
-MainWindowBase::startOSCQueue()
+MainWindowBase::startOSCQueue(bool withNetworkPort)
 {
-    m_oscQueueStarter = new OSCQueueStarter(this);
+    m_oscQueueStarter = new OSCQueueStarter(this, withNetworkPort);
     connect(m_oscQueueStarter, SIGNAL(finished()), this, SLOT(oscReady()));
     m_oscQueueStarter->start();
 }
@@ -516,10 +530,45 @@
         QTimer *oscTimer = new QTimer(this);
         connect(oscTimer, SIGNAL(timeout()), this, SLOT(pollOSC()));
         oscTimer->start(1000);
-        SVCERR << "Finished setting up OSC interface" << endl;
+
+        if (m_oscQueue->hasPort()) {
+            SVDEBUG << "Finished setting up OSC interface" << endl;
+        } else {
+            SVDEBUG << "Finished setting up internal-only OSC queue" << endl;
+        }
+
+        if (m_oscScriptFile != QString()) {
+            startOSCScript();
+        }
     }
 }
 
+void
+MainWindowBase::startOSCScript()
+{
+    m_oscScript = new OSCScript(m_oscScriptFile, m_oscQueue);
+    connect(m_oscScript, SIGNAL(finished()),
+            this, SLOT(oscScriptFinished()));
+    m_oscScriptFile = QString();
+    m_oscScript->start();
+}
+
+void
+MainWindowBase::cueOSCScript(QString fileName)
+{
+    m_oscScriptFile = fileName;
+    if (m_oscQueue && m_oscQueue->isOK()) {
+        startOSCScript();
+    }
+}
+
+void
+MainWindowBase::oscScriptFinished()
+{
+    delete m_oscScript;
+    m_oscScript = 0;
+}
+
 QString
 MainWindowBase::getOpenFileName(FileFinder::FileType type)
 {
@@ -699,16 +748,46 @@
 }
 
 void
+MainWindowBase::updateWindowTitle()
+{
+    QString title;
+
+    if (m_sessionFile != "") {
+        if (m_originalLocation != "" &&
+            m_originalLocation != m_sessionFile) { // session + location
+            title = tr("%1: %2 [%3]")
+                .arg(QApplication::applicationName())
+                .arg(QFileInfo(m_sessionFile).fileName())
+                .arg(m_originalLocation);
+        } else { // session only
+            title = tr("%1: %2")
+                .arg(QApplication::applicationName())
+                .arg(QFileInfo(m_sessionFile).fileName());
+        }
+    } else {
+        if (m_originalLocation != "") { // location only
+            title = tr("%1: %2")
+                .arg(QApplication::applicationName())
+                .arg(m_originalLocation);
+        } else { // neither
+            title = QApplication::applicationName();
+        }
+    }
+    
+    if (m_documentModified) {
+        title = tr("%1 (modified)").arg(title);
+    }
+
+    setWindowTitle(title);
+}
+
+void
 MainWindowBase::documentModified()
 {
 //    SVDEBUG << "MainWindowBase::documentModified" << endl;
 
-    if (!m_documentModified) {
-        //!!! this in subclass implementation?
-        setWindowTitle(tr("%1 (modified)").arg(windowTitle()));
-    }
-
     m_documentModified = true;
+    updateWindowTitle();
     updateMenuStates();
 }
 
@@ -717,14 +796,8 @@
 {
 //    SVDEBUG << "MainWindowBase::documentRestored" << endl;
 
-    if (m_documentModified) {
-        //!!! this in subclass implementation?
-        QString wt(windowTitle());
-        wt.replace(tr(" (modified)"), "");
-        setWindowTitle(wt);
-    }
-
     m_documentModified = false;
+    updateWindowTitle();
     updateMenuStates();
 }
 
@@ -1136,46 +1209,44 @@
     if (layer) {
     
         Model *model = layer->getModel();
-        SparseOneDimensionalModel *sodm = dynamic_cast<SparseOneDimensionalModel *>
-            (model);
+        SparseOneDimensionalModel *sodm =
+            dynamic_cast<SparseOneDimensionalModel *>(model);
 
         if (sodm) {
-            SparseOneDimensionalModel::Point point(frame, "");
-
-            SparseOneDimensionalModel::Point prevPoint(0);
+            Event point(frame, "");
+            Event prevPoint(0);
             bool havePrevPoint = false;
 
-            SparseOneDimensionalModel::EditCommand *command =
-                new SparseOneDimensionalModel::EditCommand(sodm, tr("Add Point"));
+            ChangeEventsCommand *command =
+                new ChangeEventsCommand(sodm, tr("Add Point"));
 
             if (m_labeller) {
 
                 if (m_labeller->requiresPrevPoint()) {
-
-                    SparseOneDimensionalModel::PointList prevPoints =
-                        sodm->getPreviousPoints(frame);
-
-                    if (!prevPoints.empty()) {
-                        prevPoint = *prevPoints.begin();
+                    
+                    if (sodm->getNearestEventMatching
+                        (frame,
+                         [](Event) { return true; },
+                         EventSeries::Backward,
+                         prevPoint)) {
                         havePrevPoint = true;
                     }
                 }
 
                 m_labeller->setSampleRate(sodm->getSampleRate());
 
-                if (m_labeller->actingOnPrevPoint() && havePrevPoint) {
-                    command->deletePoint(prevPoint);
-                }
-
-                m_labeller->label<SparseOneDimensionalModel::Point>
+                Labeller::Relabelling relabelling = m_labeller->label
                     (point, havePrevPoint ? &prevPoint : nullptr);
 
-                if (m_labeller->actingOnPrevPoint() && havePrevPoint) {
-                    command->addPoint(prevPoint);
+                if (relabelling.first == Labeller::AppliesToPreviousEvent) {
+                    command->remove(prevPoint);
+                    command->add(relabelling.second);
+                } else {
+                    point = relabelling.second;
                 }
             }
             
-            command->addPoint(point);
+            command->add(point);
 
             command->setName(tr("Add Point at %1 s")
                              .arg(RealTime::frame2RealTime
@@ -1231,14 +1302,12 @@
 
     RegionModel *rm = dynamic_cast<RegionModel *>(layer->getModel());
     if (rm) {
-        RegionModel::Point point(alignedStart,
-                                 rm->getValueMaximum() + 1,
-                                 alignedDuration,
-                                 "");
-        RegionModel::EditCommand *command =
-            new RegionModel::EditCommand(rm, tr("Add Point"));
-        command->addPoint(point);
-        command->setName(name);
+        Event point(alignedStart,
+                    rm->getValueMaximum() + 1,
+                    alignedDuration,
+                    "");
+        ChangeEventsCommand *command = new ChangeEventsCommand(rm, name);
+        command->add(point);
         c = command->finish();
     }
 
@@ -1249,15 +1318,13 @@
 
     NoteModel *nm = dynamic_cast<NoteModel *>(layer->getModel());
     if (nm) {
-        NoteModel::Point point(alignedStart,
-                               nm->getValueMinimum(),
-                               alignedDuration,
-                               1.f,
-                               "");
-        NoteModel::EditCommand *command =
-            new NoteModel::EditCommand(nm, tr("Add Point"));
-        command->addPoint(point);
-        command->setName(name);
+        Event point(alignedStart,
+                    nm->getValueMinimum(),
+                    alignedDuration,
+                    1.f,
+                    "");
+        ChangeEventsCommand *command = new ChangeEventsCommand(nm, name);
+        command->add(point);
         c = command->finish();
     }
 
@@ -1265,25 +1332,6 @@
         CommandHistory::getInstance()->addCommand(c, false);
         return;
     }
-
-    FlexiNoteModel *fnm = dynamic_cast<FlexiNoteModel *>(layer->getModel());
-    if (fnm) {
-        FlexiNoteModel::Point point(alignedStart,
-                                    fnm->getValueMinimum(),
-                                    alignedDuration,
-                                    1.f,
-                                    "");
-        FlexiNoteModel::EditCommand *command =
-            new FlexiNoteModel::EditCommand(fnm, tr("Add Point"));
-        command->addPoint(point);
-        command->setName(name);
-        c = command->finish();
-    }
-    
-    if (c) {
-        CommandHistory::getInstance()->addCommand(c, false);
-        return;
-    }
 }
 
 void
@@ -1306,9 +1354,10 @@
 
     Labeller labeller(*m_labeller);
     labeller.setSampleRate(sodm->getSampleRate());
-
+/*!!! to be updated after SODM API update
     Command *c = labeller.labelAll<SparseOneDimensionalModel::Point>(*sodm, &ms);
     if (c) CommandHistory::getInstance()->addCommand(c, false);
+*/
 }
 
 void
@@ -1332,9 +1381,12 @@
     Labeller labeller(*m_labeller);
     labeller.setSampleRate(sodm->getSampleRate());
 
+    (void)n;
+/*!!! to be updated after SODM API update
     Command *c = labeller.subdivide<SparseOneDimensionalModel::Point>
         (*sodm, &ms, n);
     if (c) CommandHistory::getInstance()->addCommand(c, false);
+*/
 }
 
 void
@@ -1358,9 +1410,12 @@
     Labeller labeller(*m_labeller);
     labeller.setSampleRate(sodm->getSampleRate());
 
+    (void)n;
+/*!!! to be updated after SODM API update
     Command *c = labeller.winnow<SparseOneDimensionalModel::Point>
         (*sodm, &ms, n);
     if (c) CommandHistory::getInstance()->addCommand(c, false);
+*/
 }
 
 MainWindowBase::FileOpenStatus
@@ -1632,7 +1687,7 @@
     }
 
     emit activity(tr("Import audio file \"%1\"").arg(source.getLocation()));
-
+    
     if (mode == ReplaceMainModel) {
 
         Model *prevMain = getMainModel();
@@ -1648,22 +1703,15 @@
 
         setupMenus();
 
+        m_originalLocation = source.getLocation();
+
         if (loadedTemplate || (m_sessionFile == "")) {
-            //!!! shouldn't be dealing directly with title from here -- call a method
-            setWindowTitle(tr("%1: %2")
-                           .arg(QApplication::applicationName())
-                           .arg(source.getLocation()));
             CommandHistory::getInstance()->clear();
             CommandHistory::getInstance()->documentSaved();
             m_documentModified = false;
         } else {
-            setWindowTitle(tr("%1: %2 [%3]")
-                           .arg(QApplication::applicationName())
-                           .arg(QFileInfo(m_sessionFile).fileName())
-                           .arg(source.getLocation()));
             if (m_documentModified) {
                 m_documentModified = false;
-                documentModified(); // so as to restore "(modified)" window title
             }
         }
 
@@ -1671,6 +1719,8 @@
             m_audioFile = source.getLocalFilename();
         }
 
+        updateWindowTitle();
+        
     } else if (mode == CreateAdditionalModel) {
 
         SVCERR << "Mode is CreateAdditionalModel" << endl;
@@ -2133,10 +2183,6 @@
 
         emit activity(tr("Import session file \"%1\"").arg(source.getLocation()));
 
-        setWindowTitle(tr("%1: %2")
-                       .arg(QApplication::applicationName())
-                       .arg(source.getLocation()));
-
         if (!source.isRemote() && !m_document->isIncomplete()) {
             // Setting the session file path enables the Save (as
             // opposed to Save As...) option. We can't do this if we
@@ -2153,6 +2199,7 @@
                  QMessageBox::Ok);
         }
 
+        updateWindowTitle();
         setupMenus();
         findTimeRulerLayer();
 
@@ -2169,12 +2216,13 @@
                                        source.getLocalFilename());
         }
 
+        m_originalLocation = source.getLocation();
+        
         emit sessionLoaded();
 
-    } else {
-        setWindowTitle(QApplication::applicationName());
+        updateWindowTitle();
     }
-
+    
     return ok ? FileOpenSucceeded : FileOpenFailed;
 }
 
@@ -2240,8 +2288,6 @@
 
     bool ok = (error == "");
 
-    setWindowTitle(QApplication::applicationName());
-
     if (ok) {
 
         emit activity(tr("Open session template \"%1\"").arg(source.getLocation()));
@@ -2257,6 +2303,8 @@
         emit sessionLoaded();
     }
 
+    updateWindowTitle();
+
     return ok ? FileOpenSucceeded : FileOpenFailed;
 }
 
@@ -2279,13 +2327,11 @@
 
     setupMenus();
     findTimeRulerLayer();
-    
-    setWindowTitle(tr("%1: %2")
-                   .arg(QApplication::applicationName())
-                   .arg(source.getLocation()));
+
     CommandHistory::getInstance()->clear();
     CommandHistory::getInstance()->documentSaved();
     m_documentModified = false;
+    updateWindowTitle();
 
     emit sessionLoaded();
 
@@ -2649,6 +2695,8 @@
     connect(m_document, SIGNAL(alignmentFailed(QString)),
             this, SLOT(alignmentFailed(QString)));
 
+    m_document->setAutoAlignment(m_viewManager->getAlignMode());
+
     emit replacedDocument();
 }
 
@@ -2736,6 +2784,79 @@
     }
 }
 
+bool
+MainWindowBase::exportLayerTo(Layer *layer, QString path, QString &error)
+{
+    if (QFileInfo(path).suffix() == "") path += ".svl";
+
+    QString suffix = QFileInfo(path).suffix().toLower();
+
+    Model *model = layer->getModel();
+
+    if (suffix == "xml" || suffix == "svl") {
+
+        QFile file(path);
+        if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+            error = tr("Failed to open file %1 for writing").arg(path);
+        } else {
+            QTextStream out(&file);
+            out.setCodec(QTextCodec::codecForName("UTF-8"));
+            out << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+                << "<!DOCTYPE sonic-visualiser>\n"
+                << "<sv>\n"
+                << "  <data>\n";
+
+            model->toXml(out, "    ");
+
+            out << "  </data>\n"
+                << "  <display>\n";
+
+            layer->toXml(out, "    ");
+
+            out << "  </display>\n"
+                << "</sv>\n";
+        }
+
+    } else if (suffix == "mid" || suffix == "midi") {
+
+        NoteModel *nm = dynamic_cast<NoteModel *>(model);
+
+        if (!nm) {
+            error = tr("Can't export non-note layers to MIDI");
+        } else {
+            MIDIFileWriter writer(path, nm, nm->getSampleRate());
+            writer.write();
+            if (!writer.isOK()) {
+                error = writer.getError();
+            }
+        }
+
+    } else if (suffix == "ttl" || suffix == "n3") {
+
+        if (!RDFExporter::canExportModel(model)) {
+            error = tr("Sorry, cannot export this layer type to RDF (supported types are: region, note, text, time instants, time values)");
+        } else {
+            RDFExporter exporter(path, model);
+            exporter.write();
+            if (!exporter.isOK()) {
+                error = exporter.getError();
+            }
+        }
+
+    } else {
+
+        CSVFileWriter writer(path, model,
+                             ((suffix == "csv") ? "," : "\t"));
+        writer.write();
+
+        if (!writer.isOK()) {
+            error = writer.getError();
+        }
+    }
+
+    return (error == "");
+}
+
 void
 MainWindowBase::toXml(QTextStream &out, bool asTemplate)
 {
@@ -3181,25 +3302,16 @@
         setupMenus();
         findTimeRulerLayer();
 
+        m_originalLocation = model->getLocation();
+        
         if (loadedTemplate || (m_sessionFile == "")) {
-            //!!! shouldn't be dealing directly with title from here -- call a method
-            setWindowTitle(tr("%1: %2")
-                           .arg(QApplication::applicationName())
-                           .arg(model->getLocation()));
             CommandHistory::getInstance()->clear();
             CommandHistory::getInstance()->documentSaved();
-            m_documentModified = false;
-        } else {
-            setWindowTitle(tr("%1: %2 [%3]")
-                           .arg(QApplication::applicationName())
-                           .arg(QFileInfo(m_sessionFile).fileName())
-                           .arg(model->getLocation()));
-            if (m_documentModified) {
-                m_documentModified = false;
-                documentModified(); // so as to restore "(modified)" window title
-            }
         }
 
+        m_documentModified = false;
+        updateWindowTitle();
+
     } else {
 
         CommandHistory::getInstance()->startCompoundOperation
--- a/framework/MainWindowBase.h	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/MainWindowBase.h	Fri May 17 09:46:22 2019 +0100
@@ -34,6 +34,7 @@
 #include "data/fileio/FileFinder.h"
 #include "data/fileio/FileSource.h"
 #include "data/osc/OSCQueue.h"
+#include "data/osc/OSCMessageCallback.h"
 #include <map>
 
 class Document;
@@ -58,6 +59,7 @@
 class QTreeView;
 class QPushButton;
 class OSCMessage;
+class OSCScript;
 class MIDIInput;
 class KeyReference;
 class Labeller;
@@ -81,7 +83,9 @@
  * to use different subclasses retaining the same general structure.
  */
 
-class MainWindowBase : public QMainWindow, public FrameTimer
+class MainWindowBase : public QMainWindow,
+                       public FrameTimer,
+                       public OSCMessageCallback
 {
     Q_OBJECT
 
@@ -135,6 +139,10 @@
     virtual bool saveSessionFile(QString path);
     virtual bool saveSessionTemplate(QString path);
 
+    virtual bool exportLayerTo(Layer *layer, QString path, QString &error);
+
+    void cueOSCScript(QString filename);
+    
     /// Implementation of FrameTimer interface method
     sv_frame_t getFrame() const override;
 
@@ -300,6 +308,7 @@
 
     virtual void updateMenuStates();
     virtual void updateDescriptionLabel() = 0;
+    virtual void updateWindowTitle();
 
     virtual void modelGenerationFailed(QString, QString) = 0;
     virtual void modelGenerationWarning(QString, QString) = 0;
@@ -320,7 +329,7 @@
 
     virtual void oscReady();
     virtual void pollOSC();
-    virtual void handleOSCMessage(const OSCMessage &) = 0;
+    virtual void oscScriptFinished();
 
     virtual void contextHelpChanged(const QString &);
     virtual void inProgressSelectionChanged();
@@ -337,15 +346,23 @@
     virtual void menuActionMapperInvoked(QObject *);
 
 protected:
-    QString                  m_sessionFile;
-    QString                  m_audioFile;
-    Document                *m_document;
+    QString m_sessionFile;
+    QString m_audioFile;
+    Document *m_document;
 
-    PaneStack               *m_paneStack;
-    ViewManager             *m_viewManager;
-    Layer                   *m_timeRulerLayer;
+    // This is used in the window title. It's the upstream location
+    // (maybe a URL) the user provided as source of the main model. It
+    // should be set in cases where there is no current session file
+    // and m_sessionFile is empty, or where a new main model has been
+    // imported into an existing session. It should be used only for
+    // user presentation, never parsed - treat it as an opaque label
+    QString m_originalLocation;
 
-    SoundOptions             m_soundOptions;
+    PaneStack *m_paneStack;
+    ViewManager *m_viewManager;
+    Layer *m_timeRulerLayer;
+
+    SoundOptions m_soundOptions;
 
     AudioCallbackPlaySource *m_playSource;
     AudioCallbackRecordTarget *m_recordTarget;
@@ -356,18 +373,27 @@
     class OSCQueueStarter : public QThread
     {
     public:
-        OSCQueueStarter(MainWindowBase *mwb) : QThread(mwb), m_mwb(mwb) { }
+        OSCQueueStarter(MainWindowBase *mwb, bool withNetworkPort) :
+            QThread(mwb), m_mwb(mwb), m_withPort(withNetworkPort) { }
+
         void run() override {
-            OSCQueue *queue = new OSCQueue(); // can take a long time
+            // NB creating the queue object can take a long time
+            OSCQueue *queue = new OSCQueue(m_withPort);
             m_mwb->m_oscQueue = queue;
         }
+        
     private:
         MainWindowBase *m_mwb;
+        bool m_withPort;
     };
 
     OSCQueue                *m_oscQueue;
     OSCQueueStarter         *m_oscQueueStarter;
-    void startOSCQueue();
+    OSCScript               *m_oscScript;
+    QString                  m_oscScriptFile;
+
+    void startOSCQueue(bool withNetworkPort);
+    void startOSCScript();
 
     MIDIInput               *m_midiInput;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/OSCScript.h	Fri May 17 09:46:22 2019 +0100
@@ -0,0 +1,131 @@
+/* -*- 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_OSC_SCRIPT_H
+#define SV_OSC_SCRIPT_H
+
+#include <QThread>
+#include <QFile>
+#include <QTextStream>
+
+#include "base/Debug.h"
+#include "base/StringBits.h"
+#include "data/osc/OSCQueue.h"
+#include "data/osc/OSCMessage.h"
+
+#include <stdexcept>
+
+class OSCScript : public QThread
+{
+    Q_OBJECT
+
+public:
+    OSCScript(QString filename, OSCQueue *queue) :
+        m_filename(filename),
+        m_queue(queue),
+        m_abandoning(false) {
+    }
+
+    void run() override {
+            
+        if (!m_queue) {
+            SVCERR << "OSCScript: No OSC queue available" << endl;
+            throw std::runtime_error("OSC queue not running");
+        }
+
+        QFile f;
+        QString reportedFilename;
+
+        if (m_filename == "-") {
+            f.open(stdin, QFile::ReadOnly | QFile::Text);
+            reportedFilename = "<stdin>";
+        } else {
+            f.setFileName(m_filename);
+            if (!f.open(QFile::ReadOnly | QFile::Text)) {
+                SVCERR << "OSCScript: Failed to open script file \""
+                       << m_filename << "\" for reading" << endl;
+                throw std::runtime_error("OSC script file not found");
+            }
+            reportedFilename = m_filename;
+        }
+        
+        QTextStream str(&f);
+        int lineno = 0;
+
+        while (!str.atEnd() && !m_abandoning) {
+
+            ++lineno;
+
+            QString line = str.readLine().trimmed();
+            if (line == QString()) continue;
+
+            if (line[0] == '#') {
+                continue;
+
+            } else if (line[0].isDigit()) {
+                bool ok = false;
+                float pause = line.toFloat(&ok);
+                if (ok) {
+                    SVCERR << "OSCScript: "
+                           << reportedFilename << ":" << lineno
+                           << ": pausing for " << pause << " sec" << endl;
+                    msleep(unsigned(round(pause * 1000.0f)));
+                    continue;
+                } else {
+                    SVCERR << "OSCScript: "
+                           << reportedFilename << ":" << lineno
+                           << ": warning: failed to parse sleep time, ignoring"
+                           << endl;
+                    continue;
+                }
+
+            } else if (line[0] == '/' && line.size() > 1) {
+                QStringList parts = StringBits::splitQuoted(line, ' ');
+                if (parts.empty()) {
+                    SVCERR << "OSCScript: "
+                           << reportedFilename << ":" << lineno
+                           << ": warning: empty command spec, ignoring"
+                           << endl;
+                    continue;
+                }
+                OSCMessage message;
+                message.setMethod(parts[0].mid(1));
+                for (int i = 1; i < parts.size(); ++i) {
+                    message.addArg(parts[i]);
+                }
+                SVCERR << "OSCScript: " << reportedFilename << ":" << lineno
+                       << ": invoking: \"" << parts[0] << "\"" << endl;
+                m_queue->postMessage(message);
+
+            } else {
+                SVCERR << "OSCScript: " << reportedFilename << ":" << lineno
+                       << ": warning: message expected, ignoring" << endl;
+            }
+        }
+
+        SVCERR << "OSCScript: " << reportedFilename << ": finished" << endl;
+    }
+
+    void abandon() {
+        m_abandoning = true;
+    }
+    
+private:
+    QString m_filename;
+    OSCQueue *m_queue; // I do not own this
+    bool m_abandoning;
+};
+
+#endif
+
--- a/framework/SVFileReader.cpp	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/SVFileReader.cpp	Fri May 17 09:46:22 2019 +0100
@@ -31,7 +31,6 @@
 #include "data/model/SparseOneDimensionalModel.h"
 #include "data/model/SparseTimeValueModel.h"
 #include "data/model/NoteModel.h"
-#include "data/model/FlexiNoteModel.h"
 #include "data/model/RegionModel.h"
 #include "data/model/TextModel.h"
 #include "data/model/ImageModel.h"
@@ -459,28 +458,30 @@
 {
     makeAggregateModels();
     
-    std::set<Model *> unaddedModels;
-    
     for (std::map<int, Model *>::iterator i = m_models.begin();
          i != m_models.end(); ++i) {
-        if (m_addedModels.find(i->second) == m_addedModels.end()) {
-            unaddedModels.insert(i->second);
+
+        Model *model = i->second;
+
+        if (m_addedModels.find(model) != m_addedModels.end()) {
+            // already added this one
+            continue;
         }
-    }
-    
-    for (std::set<Model *>::iterator i = unaddedModels.begin();
-         i != unaddedModels.end(); ++i) {
-        Model *model = *i;
-        // don't want to add these models, because their lifespans
-        // are entirely dictated by the models that "own" them even
-        // though they were read independently from the .sv file.
-        // (pity we don't have a nicer way)
+        
+        // don't want to add path and alignment models to the
+        // document, because their lifespans are entirely dictated by
+        // the models that "own" them even though they were read
+        // independently from the .sv file.  (pity we don't have a
+        // nicer way to handle this)
         if (!dynamic_cast<PathModel *>(model) &&
             !dynamic_cast<AlignmentModel *>(model)) {
+            
             m_document->addImportedModel(model);
         }
-        // but we add all models here, so they don't get deleted
-        // when the file loader is destroyed
+        
+        // but we add all models including path and alignment ones to
+        // the added set, so they don't get deleted from our own
+        // destructor
         m_addedModels.insert(model);
     }
 }
@@ -713,13 +714,16 @@
                     model->setObjectName(name);
                     m_models[id] = model;
                 } else if (attributes.value("subtype") == "flexinote") {
-                    FlexiNoteModel *model;
+                    NoteModel *model;
                     if (haveMinMax) {
-                        model = new FlexiNoteModel
-                            (sampleRate, resolution, minimum, maximum, notifyOnAdd);
+                        model = new NoteModel
+                            (sampleRate, resolution, minimum, maximum,
+                             notifyOnAdd,
+                             NoteModel::FLEXI_NOTE);
                     } else {
-                        model = new FlexiNoteModel
-                            (sampleRate, resolution, notifyOnAdd);
+                        model = new NoteModel
+                            (sampleRate, resolution, notifyOnAdd,
+                             NoteModel::FLEXI_NOTE);
                     }
                     model->setValueQuantization(valueQuantization);
                     model->setScaleUnits(units);
@@ -1051,7 +1055,6 @@
 
     case 3:
         if (dynamic_cast<NoteModel *>(model)) good = true;
-        else if (dynamic_cast<FlexiNoteModel *>(model)) good = true;
         else if (dynamic_cast<RegionModel *>(model)) good = true;
         else if (dynamic_cast<EditableDenseThreeDimensionalModel *>(model)) {
             m_datasetSeparator = attributes.value("separator");
@@ -1085,7 +1088,7 @@
     if (sodm) {
 //        SVCERR << "Current dataset is a sparse one dimensional model" << endl;
         QString label = attributes.value("label");
-        sodm->addPoint(SparseOneDimensionalModel::Point(frame, label));
+        sodm->add(Event(frame, label));
         return true;
     }
 
@@ -1097,7 +1100,7 @@
         float value = 0.0;
         value = attributes.value("value").trimmed().toFloat(&ok);
         QString label = attributes.value("label");
-        stvm->addPoint(SparseTimeValueModel::Point(frame, value, label));
+        stvm->add(Event(frame, value, label));
         return ok;
     }
         
@@ -1115,25 +1118,7 @@
             level = 1.f;
             ok = true;
         }
-        nm->addPoint(NoteModel::Point(frame, value, duration, level, label));
-        return ok;
-    }
-
-    FlexiNoteModel *fnm = dynamic_cast<FlexiNoteModel *>(m_currentDataset);
-
-    if (fnm) {
-//        SVCERR << "Current dataset is a flexinote model" << endl;
-        float value = 0.0;
-        value = attributes.value("value").trimmed().toFloat(&ok);
-        int duration = 0;
-        duration = attributes.value("duration").trimmed().toInt(&ok);
-        QString label = attributes.value("label");
-        float level = attributes.value("level").trimmed().toFloat(&ok);
-        if (!ok) { // level is optional
-            level = 1.f;
-            ok = true;
-        }
-        fnm->addPoint(FlexiNoteModel::Point(frame, value, duration, level, label));
+        nm->add(Event(frame, value, duration, level, label));
         return ok;
     }
 
@@ -1146,7 +1131,7 @@
         int duration = 0;
         duration = attributes.value("duration").trimmed().toInt(&ok);
         QString label = attributes.value("label");
-        rm->addPoint(RegionModel::Point(frame, value, duration, label));
+        rm->add(Event(frame, value, duration, label));
         return ok;
     }
 
@@ -1158,7 +1143,7 @@
         height = attributes.value("height").trimmed().toFloat(&ok);
         QString label = attributes.value("label");
 //        SVDEBUG << "SVFileReader::addPointToDataset: TextModel: frame = " << frame << ", height = " << height << ", label = " << label << ", ok = " << ok << endl;
-        tm->addPoint(TextModel::Point(frame, height, label));
+        tm->add(Event(frame, height, label));
         return ok;
     }
 
@@ -1168,7 +1153,7 @@
 //        SVCERR << "Current dataset is a path model" << endl;
         int mapframe = attributes.value("mapframe").trimmed().toInt(&ok);
 //        SVDEBUG << "SVFileReader::addPointToDataset: PathModel: frame = " << frame << ", mapframe = " << mapframe << ", ok = " << ok << endl;
-        pm->addPoint(PathModel::Point(frame, mapframe));
+        pm->add(PathPoint(frame, mapframe));
         return ok;
     }
 
@@ -1179,7 +1164,7 @@
         QString image = attributes.value("image");
         QString label = attributes.value("label");
 //        SVDEBUG << "SVFileReader::addPointToDataset: ImageModel: frame = " << frame << ", image = " << image << ", label = " << label << ", ok = " << ok << endl;
-        im->addPoint(ImageModel::Point(frame, image, label));
+        im->add(Event(frame).withURI(image).withLabel(label));
         return ok;
     }