changeset 666:21673429dba5 single-point

Merge from default branch
author Chris Cannam
date Wed, 24 Apr 2019 11:45:31 +0100
parents 06db8f3ceb95 (diff) e19c609a7bec (current diff)
children 31ea416fea3c
files framework/Align.cpp framework/Document.cpp framework/SVFileReader.cpp
diffstat 9 files changed, 601 insertions(+), 301 deletions(-) [+]
line wrap: on
line diff
--- a/audio/AudioGenerator.cpp	Thu Apr 04 16:17:11 2019 +0100
+++ b/audio/AudioGenerator.cpp	Wed Apr 24 11:45:31 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	Wed Apr 24 11:45:31 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	Wed Apr 24 11:45:31 2019 +0100
@@ -139,7 +139,7 @@
         (transformOutput);
 
     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;
@@ -192,7 +192,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;
     }
     
@@ -223,8 +223,8 @@
     bool success = process->waitForStarted();
 
     if (!success) {
-        cerr << "ERROR: Align::alignModelViaProgram: Program did not start"
-             << endl;
+        SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
+               << endl;
         m_error = "Alignment program could not be started";
         m_processModels.erase(process);
         rm->setAlignment(nullptr); // deletes alignmentModel as well
@@ -237,13 +237,13 @@
 void
 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
 {
-    cerr << "Align::alignmentProgramFinished" << endl;
+    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;
+        SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
+               << " not found in process model map!" << endl;
         return;
     }
 
@@ -269,8 +269,8 @@
 
         CSVFileReader reader(process, format, alignmentModel->getSampleRate());
         if (!reader.isOK()) {
-            cerr << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
-                 << endl;
+            SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
+                   << endl;
             m_error = QString("Failed to parse output of program: %1")
                 .arg(reader.getError());
             goto done;
@@ -280,30 +280,30 @@
 
         SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput);
         if (!path) {
-            cerr << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
-                 << endl;
+            SVCERR << "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");
             goto done;
         }
 
-        if (path->getPoints().empty()) {
-            cerr << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
-                 << endl;
+        if (path->isEmpty()) {
+            SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
+                   << endl;
             m_error = QString("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;
+        SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
+               << "failed: exit code " << exitCode << ", status " << status
+               << endl;
         m_error = "Aligner process returned non-zero exit status";
     }
 
--- a/framework/Document.cpp	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/Document.cpp	Wed Apr 24 11:45:31 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 \""
@@ -592,21 +591,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 +615,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 +633,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 +670,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 +708,7 @@
     connect(model, SIGNAL(modelInvalidated()),
             this, SLOT(aggregateModelInvalidated()));
     m_aggregateModels.insert(model);
+    SVDEBUG << "Document::addAggregateModel(" << model << ")" << endl;
 }
 
 void
@@ -713,6 +716,7 @@
 {
     QObject *s = sender();
     AggregateWaveModel *aggregate = qobject_cast<AggregateWaveModel *>(s);
+    SVDEBUG << "Document::aggregateModelInvalidated(" << aggregate << ")" << endl;
     if (aggregate) releaseModel(aggregate);
 }
 
@@ -721,12 +725,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 +775,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 +803,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 +832,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 +846,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,7 +865,7 @@
     if (m_layerViewMap.find(layer) != m_layerViewMap.end() &&
         m_layerViewMap[layer].size() > 0) {
 
-        cerr << "WARNING: Document::deleteLayer: Layer "
+        SVCERR << "WARNING: Document::deleteLayer: Layer "
                   << layer << " [" << layer->objectName() << "]"
                   << " is still used in " << m_layerViewMap[layer].size()
                   << " views!" << endl;
@@ -868,7 +873,7 @@
         if (force) {
 
 #ifdef DEBUG_DOCUMENT
-            cerr << "(force flag set -- deleting from all views)" << endl;
+            SVCERR << "(force flag set -- deleting from all views)" << endl;
 #endif
 
             for (std::set<View *>::iterator j = m_layerViewMap[layer].begin();
@@ -885,7 +890,15 @@
         }
     }
 
-    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 +906,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 +922,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 +943,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 +955,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 +974,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 +1007,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 +1022,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 +1046,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 +1068,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 +1086,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
@@ -1122,8 +1139,8 @@
 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);
 }
@@ -1275,6 +1292,10 @@
 
     if (m_mainModel) {
 
+#ifdef DEBUG_DOCUMENT
+        SVDEBUG << "Document::toXml: writing main model" << endl;
+#endif
+        
         if (asTemplate) {
             writePlaceholderMainModel(out, indent + "  ");
         } else {
@@ -1287,8 +1308,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 +1351,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 +1383,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 +1454,7 @@
                 playParameters->toXml
                     (out, indent + "  ",
                      QString("model=\"%1\"")
-                     .arg(XmlExportable::getObjectExportId(model)));
+                     .arg(model->getExportId()));
             }
         }
     }
@@ -1439,9 +1475,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 +1487,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 +1526,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 +1551,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	Wed Apr 24 11:45:31 2019 +0100
@@ -370,6 +370,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 +380,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 +481,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	Wed Apr 24 11:45:31 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)
 {
@@ -1136,46 +1185,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 +1278,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 +1294,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 +1308,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 +1330,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 +1357,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 +1386,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
@@ -2736,6 +2767,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)
 {
--- a/framework/MainWindowBase.h	Thu Apr 04 16:17:11 2019 +0100
+++ b/framework/MainWindowBase.h	Wed Apr 24 11:45:31 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;
 
@@ -320,7 +328,7 @@
 
     virtual void oscReady();
     virtual void pollOSC();
-    virtual void handleOSCMessage(const OSCMessage &) = 0;
+    virtual void oscScriptFinished();
 
     virtual void contextHelpChanged(const QString &);
     virtual void inProgressSelectionChanged();
@@ -356,18 +364,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	Wed Apr 24 11:45:31 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	Wed Apr 24 11:45:31 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;
     }