changeset 778:83a7b10b7415

Merge from branch pitch-align
author Chris Cannam
date Fri, 26 Jun 2020 13:48:52 +0100
parents 7bded7599874 (current diff) 87d33e79855b (diff)
children 5de2b710cfae
files framework/Document.cpp
diffstat 15 files changed, 1633 insertions(+), 531 deletions(-) [+]
line wrap: on
line diff
--- a/align/Align.cpp	Wed Jun 03 13:58:29 2020 +0100
+++ b/align/Align.cpp	Fri Jun 26 13:48:52 2020 +0100
@@ -13,20 +13,67 @@
 */
 
 #include "Align.h"
-#include "TransformAligner.h"
+
+#include "LinearAligner.h"
+#include "MATCHAligner.h"
+#include "TransformDTWAligner.h"
 #include "ExternalProgramAligner.h"
+
 #include "framework/Document.h"
 
+#include "transform/Transform.h"
+#include "transform/TransformFactory.h"
+
+#include "base/Pitch.h"
+
 #include <QSettings>
 #include <QTimer>
 
+using std::make_shared;
+
+QString
+Align::getAlignmentTypeTag(AlignmentType type)
+{
+    switch (type) {
+    case NoAlignment:
+    default:
+        return "no-alignment";
+    case LinearAlignment:
+        return "linear-alignment";
+    case TrimmedLinearAlignment:
+        return "trimmed-linear-alignment";
+    case MATCHAlignment:
+        return "match-alignment";
+    case MATCHAlignmentWithPitchCompare:
+        return "match-alignment-with-pitch";
+    case SungNoteContourAlignment:
+        return "sung-note-alignment";
+    case TransformDrivenDTWAlignment:
+        return "transform-driven-alignment";
+    case ExternalProgramAlignment:
+        return "external-program-alignment";
+    }
+}
+
+Align::AlignmentType
+Align::getAlignmentTypeForTag(QString tag)
+{
+    for (int i = 0; i <= int(LastAlignmentType); ++i) {
+        if (tag == getAlignmentTypeTag(AlignmentType(i))) {
+            return AlignmentType(i);
+        }
+    }
+    return NoAlignment;
+}
+
 void
 Align::alignModel(Document *doc,
                   ModelId reference,
                   ModelId toAlign)
 {
-    addAligner(doc, reference, toAlign);
-    m_aligners[toAlign]->begin();
+    if (addAligner(doc, reference, toAlign)) {
+        m_aligners[toAlign]->begin();
+    }
 }
 
 void
@@ -34,47 +81,116 @@
                          ModelId reference,
                          ModelId toAlign)
 {
-    addAligner(doc, reference, toAlign);
-    int delay = 500 + 500 * int(m_aligners.size());
+    int delay = 700 * int(m_aligners.size());
     if (delay > 3500) {
         delay = 3500;
     }
+    if (!addAligner(doc, reference, toAlign)) {
+        return;
+    }
     SVCERR << "Align::scheduleAlignment: delaying " << delay << "ms" << endl;
     QTimer::singleShot(delay, m_aligners[toAlign].get(), SLOT(begin()));
 }
 
-void
+bool
 Align::addAligner(Document *doc,
                   ModelId reference,
                   ModelId toAlign)
 {
-    bool useProgram;
-    QString program;
-    getAlignerPreference(useProgram, program);
+    AlignmentType type = getAlignmentPreference();
     
     std::shared_ptr<Aligner> aligner;
 
+    if (m_aligners.find(toAlign) != m_aligners.end()) {
+        // We don't want a callback on removeAligner to happen during
+        // our own call to addAligner! Disconnect and delete the old
+        // aligner first
+        disconnect(m_aligners[toAlign].get(), nullptr, this, nullptr);
+        m_aligners.erase(toAlign);
+    }
+    
     {
         // Replace the aligner with a new one. This also stops any
         // previously-running alignment, when the old entry is
         // replaced and its aligner destroyed.
         
         QMutexLocker locker(&m_mutex);
-    
-        if (useProgram && (program != "")) {
-            m_aligners[toAlign] =
-                std::make_shared<ExternalProgramAligner>(doc,
-                                                         reference,
-                                                         toAlign,
-                                                         program);
-        } else {
-            m_aligners[toAlign] =
-                std::make_shared<TransformAligner>(doc,
-                                                   reference,
-                                                   toAlign);
+
+        switch (type) {
+
+        case NoAlignment:
+            return false;
+
+        case LinearAlignment:
+        case TrimmedLinearAlignment: {
+            bool trimmed = (type == TrimmedLinearAlignment);
+            aligner = make_shared<LinearAligner>(doc,
+                                                 reference,
+                                                 toAlign,
+                                                 trimmed);
+            break;
         }
 
-        aligner = m_aligners[toAlign];
+        case MATCHAlignment:
+        case MATCHAlignmentWithPitchCompare: {
+
+            bool withTuningDifference =
+                (type == MATCHAlignmentWithPitchCompare);
+            
+            aligner = make_shared<MATCHAligner>(doc,
+                                                reference,
+                                                toAlign,
+                                                withTuningDifference);
+            break;
+        }
+
+        case SungNoteContourAlignment:
+        {
+            auto refModel = ModelById::get(reference);
+            if (!refModel) return false;
+
+            Transform transform = TransformFactory::getInstance()->
+                getDefaultTransformFor("vamp:pyin:pyin:notes",
+                                       refModel->getSampleRate());
+
+            aligner = make_shared<TransformDTWAligner>
+                (doc,
+                 reference,
+                 toAlign,
+                 transform,
+                 [](double prev, double curr) {
+                     RiseFallDTW::Value v;
+                     if (curr <= 0.0) {
+                         v = { RiseFallDTW::Direction::None, 0.0 };
+                     } else if (prev <= 0.0) {
+                         v = { RiseFallDTW::Direction::Up, 0.0 };
+                     } else {
+                         double prevP = Pitch::getPitchForFrequency(prev);
+                         double currP = Pitch::getPitchForFrequency(curr);
+                         if (currP >= prevP) {
+                             v = { RiseFallDTW::Direction::Up, currP - prevP };
+                         } else {
+                             v = { RiseFallDTW::Direction::Down, prevP - currP };
+                         }
+                     }
+                     return v;
+                 });
+            break;
+        }
+        
+        case TransformDrivenDTWAlignment:
+            throw std::logic_error("Not yet implemented"); //!!!
+
+        case ExternalProgramAlignment: {
+            aligner = make_shared<ExternalProgramAligner>
+                (doc,
+                 reference,
+                 toAlign,
+                 getPreferredAlignmentProgram());
+        }
+        }
+
+        m_aligners[toAlign] = aligner;
     }
 
     connect(aligner.get(), SIGNAL(complete(ModelId)),
@@ -82,29 +198,75 @@
 
     connect(aligner.get(), SIGNAL(failed(ModelId, QString)),
             this, SLOT(alignerFailed(ModelId, QString)));
+
+    return true;
+}
+
+Align::AlignmentType
+Align::getAlignmentPreference()
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    QString tag = settings.value
+        ("alignment-type", getAlignmentTypeTag(MATCHAlignment)).toString();
+    return getAlignmentTypeForTag(tag);
+}
+
+QString
+Align::getPreferredAlignmentProgram()
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    return settings.value("alignment-program", "").toString();
+}
+
+Transform
+Align::getPreferredAlignmentTransform()
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    QString xml = settings.value("alignment-transform", "").toString();
+    return Transform(xml);
 }
 
 void
-Align::getAlignerPreference(bool &useProgram, QString &program)
+Align::setAlignmentPreference(AlignmentType type)
 {
     QSettings settings;
-    settings.beginGroup("Preferences");
-    useProgram = settings.value("use-external-alignment", false).toBool();
-    program = settings.value("external-alignment-program", "").toString();
+    settings.beginGroup("Alignment");
+    QString tag = getAlignmentTypeTag(type);
+    settings.setValue("alignment-type", tag);
+    settings.endGroup();
+}
+
+void
+Align::setPreferredAlignmentProgram(QString program)
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    settings.setValue("alignment-program", program);
+    settings.endGroup();
+}
+
+void
+Align::setPreferredAlignmentTransform(Transform transform)
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    settings.setValue("alignment-transform", transform.toXmlString());
     settings.endGroup();
 }
 
 bool
 Align::canAlign() 
 {
-    bool useProgram;
-    QString program;
-    getAlignerPreference(useProgram, program);
+    AlignmentType type = getAlignmentPreference();
 
-    if (useProgram) {
-        return ExternalProgramAligner::isAvailable(program);
+    if (type == ExternalProgramAlignment) {
+        return ExternalProgramAligner::isAvailable
+            (getPreferredAlignmentProgram());
     } else {
-        return TransformAligner::isAvailable();
+        return MATCHAligner::isAvailable();
     }
 }
 
--- a/align/Align.h	Wed Jun 03 13:58:29 2020 +0100
+++ b/align/Align.h	Fri Jun 26 13:48:52 2020 +0100
@@ -23,6 +23,8 @@
 
 #include "Aligner.h"
 
+#include "transform/Transform.h"
+
 class AlignmentModel;
 class Document;
 
@@ -33,11 +35,92 @@
 public:
     Align() { }
 
+    enum AlignmentType {
+        NoAlignment,
+        LinearAlignment,
+        TrimmedLinearAlignment,
+        MATCHAlignment,
+        MATCHAlignmentWithPitchCompare,
+        SungNoteContourAlignment,
+        TransformDrivenDTWAlignment,
+        ExternalProgramAlignment,
+
+        LastAlignmentType = ExternalProgramAlignment
+    };
+
+    /**
+     * Convert an alignment type to a stable machine-readable string.
+     */
+    static QString getAlignmentTypeTag(AlignmentType type);
+
+    /**
+     * Convert an alignment type back from a stable machine-readable
+     * string.
+     */
+    static AlignmentType getAlignmentTypeForTag(QString tag);
+
+    /**
+     * Get the currently set alignment preference from the global
+     * application settings. If the returned preference is
+     * TransformDrivenDTWAlignment or ExternalProgramAlignment, then
+     * it will also be necessary to query
+     * getPreferredAlignmentTransform() or
+     * getPreferredAlignmentProgram() respectively in order to get the
+     * information needed to perform an alignment.
+     */
+    static AlignmentType getAlignmentPreference();
+    
+    /**
+     * Set the alignment preference to the global application
+     * settings. If the preference is TransformDrivenDTWAlignment or
+     * ExternalProgramAlignment, you may also wish to call
+     * setPreferredAlignmentTransform() or
+     * setPreferredAlignmentProgram() respectively.
+     */
+    static void setAlignmentPreference(AlignmentType type);
+
+    /**
+     * Get the external program associated with the
+     * ExternalProgramAlignment type, if any is set (an empty string
+     * otherwise). Note that this will return a value if any has ever
+     * been set, regardless of whether ExternalProgramAlignment is the
+     * currently chosen alignment type or not.
+     */
+    static QString getPreferredAlignmentProgram();
+
+    /**
+     * Set the external program associated with the
+     * ExternalProgramAlignment type. It is not necessary for the
+     * current preferred alignment type actually to be
+     * ExternalProgramAlignment in order to change this setting.  No
+     * validation is carried out on the argument - we don't verify
+     * that it actually is the path of a program, or anything else.
+     */
+    static void setPreferredAlignmentProgram(QString program);
+    
+    /**
+     * Get the transform associated with the
+     * TransformDrivenDTWAlignment type, if any is set (a default
+     * constructed Transform otherwise). Note that this will return a
+     * value if any has ever been set, regardless of whether
+     * TransformDrivenDTWAlignment is the currently chosen alignment
+     * type or not.
+     */
+    static Transform getPreferredAlignmentTransform();
+
+    /**
+     * Set the transform associated with the
+     * TransformDrivenDTWAlignment type. It is not necessary for the
+     * current preferred alignment type actually to be
+     * TransformDrivenDTWAlignment in order to change this setting.
+     */ 
+    static void setPreferredAlignmentTransform(Transform transform);
+    
     /**
      * Align the "other" model to the reference, attaching an
      * AlignmentModel to it. Alignment is carried out by the method
-     * configured in the user preferences (either a plugin transform
-     * or an external process) and is done asynchronously. 
+     * configured in the user preferences (see
+     * getAlignmentPreference() etc) and is done asynchronously.
      *
      * Any errors are reported by firing the alignmentFailed
      * signal. Note that the signal may be fired during the call to
@@ -79,8 +162,8 @@
                            ModelId toAlign);
     
     /**
-     * Return true if the alignment facility is available (relevant
-     * plugin installed, etc).
+     * Return true if the preferred alignment facility is available
+     * (relevant plugin installed, etc).
      */
     static bool canAlign();
 
@@ -111,10 +194,8 @@
     // we don't key this on the whole (reference, toAlign) pair
     std::map<ModelId, std::shared_ptr<Aligner>> m_aligners;
 
-    void addAligner(Document *doc, ModelId reference, ModelId toAlign);
+    bool addAligner(Document *doc, ModelId reference, ModelId toAlign);
     void removeAligner(QObject *);
-
-    static void getAlignerPreference(bool &useProgram, QString &program);
 };
 
 #endif
--- a/align/Aligner.h	Wed Jun 03 13:58:29 2020 +0100
+++ b/align/Aligner.h	Fri Jun 26 13:48:52 2020 +0100
@@ -32,12 +32,16 @@
 signals:
     /**
      * Emitted when alignment is successfully completed. The reference
-     * and toAlign models can be queried from the alignment model.
+     * and toAlign models can be queried from the alignment
+     * model. This should be emitted as the last thing the aligner
+     * does, as the recipient may delete the aligner during the call.
      */
     void complete(ModelId alignmentModel); // an AlignmentModel
 
     /**
-     * Emitted when alignment fails.
+     * Emitted when alignment fails. This should be emitted as the
+     * last thing the aligner does, as the recipient may delete the
+     * aligner during the call.
      */
     void failed(ModelId toAlign, QString errorText); // the toAlign model
 };
--- a/align/DTW.h	Wed Jun 03 13:58:29 2020 +0100
+++ b/align/DTW.h	Fri Jun 26 13:48:52 2020 +0100
@@ -18,6 +18,8 @@
 #include <vector>
 #include <functional>
 
+//#define DEBUG_DTW 1
+
 template <typename Value>
 class DTW
 {
@@ -38,6 +40,16 @@
 
         auto costs = costSeries(s1, s2);
 
+#ifdef DEBUG_DTW
+        SVCERR << "Cost matrix:" << endl;
+        for (auto v: costs) {
+            for (auto x: v) {
+                SVCERR << x << " ";
+            }
+            SVCERR << "\n";
+        }
+#endif
+        
         size_t j = s1.size() - 1;
         size_t i = s2.size() - 1;
 
@@ -51,12 +63,12 @@
 
             if (a < b) {
                 --j;
-                if (both < a) {
+                if (both <= a) {
                     --i;
                 }
             } else {
                 --i;
-                if (both < b) {
+                if (both <= b) {
                     --j;
                 }
             }
@@ -189,4 +201,11 @@
     }
 };
 
+inline std::ostream &operator<<(std::ostream &s, const RiseFallDTW::Value v) {
+    return (s <<
+            (v.direction == RiseFallDTW::Direction::None ? "=" :
+             v.direction == RiseFallDTW::Direction::Up ? "+" : "-")
+            << v.distance);
+}
+
 #endif
--- a/align/ExternalProgramAligner.cpp	Wed Jun 03 13:58:29 2020 +0100
+++ b/align/ExternalProgramAligner.cpp	Fri Jun 26 13:48:52 2020 +0100
@@ -39,6 +39,10 @@
 
 ExternalProgramAligner::~ExternalProgramAligner()
 {
+    if (m_process) {
+        disconnect(m_process, nullptr, this, nullptr);
+    }
+    
     delete m_process;
 }
 
@@ -63,6 +67,11 @@
         return;
     }
 
+    if (m_program == "") {
+        emit failed(m_toAlign, tr("No external program specified"));
+        return;
+    }
+
     while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
         qApp->processEvents();
     }
@@ -114,22 +123,25 @@
     if (!success) {
         
         SVCERR << "ERROR: ExternalProgramAligner: Program did not start" << endl;
+
+        other->setAlignment({});
+        ModelById::release(m_alignmentModel);
+        delete m_process;
+        m_process = nullptr;
+
         emit failed(m_toAlign,
                     tr("Alignment program \"%1\" did not start")
                     .arg(m_program));
         
-        other->setAlignment({});
-        ModelById::release(m_alignmentModel);
-        delete m_process;
-        m_process = nullptr;
-        
     } else {
+        alignmentModel->setCompletion(10);
         m_document->addNonDerivedModel(m_alignmentModel);
     }
 }
 
 void
-ExternalProgramAligner::programFinished(int  exitCode, QProcess::ExitStatus status)
+ExternalProgramAligner::programFinished(int exitCode,
+                                        QProcess::ExitStatus status)
 {
     SVCERR << "ExternalProgramAligner::programFinished" << endl;
     
@@ -147,6 +159,8 @@
                << endl;
         return;
     }
+
+    QString errorText;
     
     if (exitCode == 0 && status == 0) {
 
@@ -170,10 +184,9 @@
         if (!reader.isOK()) {
             SVCERR << "ERROR: ExternalProgramAligner: Failed to parse output"
                    << endl;
-            QString error = tr("Failed to parse output of program: %1")
+            errorText = tr("Failed to parse output of program: %1")
                 .arg(reader.getError());
-            alignmentModel->setError(error);
-            emit failed(m_toAlign, error);
+            alignmentModel->setError(errorText);
             goto done;
         }
 
@@ -186,22 +199,20 @@
         if (!path) {
             SVCERR << "ERROR: ExternalProgramAligner: Output did not convert to sparse time-value model"
                    << endl;
-            QString error =
+            errorText =
                 tr("Output of alignment program was not in the proper format");
-            alignmentModel->setError(error);
+            alignmentModel->setError(errorText);
             delete csvOutput;
-            emit failed(m_toAlign, error);
             goto done;
         }
                        
         if (path->isEmpty()) {
             SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings"
                    << endl;
-            QString error = 
+            errorText = 
                 tr("Output of alignment program contained no mappings");
-            alignmentModel->setError(error);
+            alignmentModel->setError(errorText);
             delete path;
-            emit failed(m_toAlign, error);
             goto done;
         }
 
@@ -211,8 +222,7 @@
         auto pathId =
             ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
         alignmentModel->setPathFrom(pathId);
-
-        emit complete(m_alignmentModel);
+        alignmentModel->setCompletion(100);
 
         ModelById::release(pathId);
         
@@ -220,12 +230,19 @@
         SVCERR << "ERROR: ExternalProgramAligner: Aligner program "
                << "failed: exit code " << exitCode << ", status " << status
                << endl;
-        QString error = tr("Aligner process returned non-zero exit status");
-        alignmentModel->setError(error);
-        emit failed(m_toAlign, error);
+        errorText = tr("Aligner process returned non-zero exit status");
+        alignmentModel->setError(errorText);
     }
 
 done:
     delete m_process;
     m_process = nullptr;
+
+    // "This should be emitted as the last thing the aligner does, as
+    // the recipient may delete the aligner during the call."
+    if (errorText == "") {
+        emit complete(m_alignmentModel);
+    } else {
+        emit failed(m_toAlign, errorText);
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/LinearAligner.cpp	Fri Jun 26 13:48:52 2020 +0100
@@ -0,0 +1,172 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "LinearAligner.h"
+
+#include "system/System.h"
+
+#include "data/model/Path.h"
+#include "data/model/AlignmentModel.h"
+
+#include "framework/Document.h"
+
+#include "svcore/data/model/DenseTimeValueModel.h"
+
+#include <QApplication>
+
+LinearAligner::LinearAligner(Document *doc,
+                             ModelId reference,
+                             ModelId toAlign,
+                             bool trimmed) :
+    m_document(doc),
+    m_reference(reference),
+    m_toAlign(toAlign),
+    m_trimmed(trimmed)
+{
+}
+
+LinearAligner::~LinearAligner()
+{
+}
+
+void
+LinearAligner::begin()
+{
+    bool ready = false;
+    while (!ready) {
+        { // scope so as to release input shared_ptr before sleeping
+            auto reference = ModelById::get(m_reference);
+            auto toAlign = ModelById::get(m_toAlign);
+            if (!reference || !reference->isOK() ||
+                !toAlign || !toAlign->isOK()) {
+                return;
+            }
+            ready = (reference->isReady() && toAlign->isReady());
+        }
+        if (!ready) {
+            SVDEBUG << "LinearAligner: Waiting for models..." << endl;
+            QApplication::processEvents(QEventLoop::ExcludeUserInputEvents |
+                                        QEventLoop::ExcludeSocketNotifiers,
+                                        500);
+        }
+    }
+
+    auto reference = ModelById::get(m_reference);
+    auto toAlign = ModelById::get(m_toAlign);
+
+    if (!reference || !reference->isOK() ||
+        !toAlign || !toAlign->isOK()) {
+        return;
+    }
+
+    sv_frame_t s0, e0, s1, e1;
+    s0 = reference->getStartFrame();
+    e0 = reference->getEndFrame();
+    s1 = toAlign->getStartFrame();
+    e1 = toAlign->getEndFrame();
+
+    if (m_trimmed) {
+        getTrimmedExtents(m_reference, s0, e0);
+        getTrimmedExtents(m_toAlign, s1, e1);
+        SVCERR << "Trimmed extents: reference: " << s0 << " to " << e0
+               << ", toAlign: " << s1 << " to " << e1 << endl;
+    }
+
+    sv_frame_t d0 = e0 - s0, d1 = e1 - s1;
+
+    if (d1 == 0) {
+        return;
+    }
+    
+    double ratio = double(d0) / double(d1);
+    int resolution = 1024;
+    
+    Path path(reference->getSampleRate(), resolution);
+
+    for (sv_frame_t f = s1; f < e1; f += resolution) {
+        sv_frame_t target = s0 + sv_frame_t(double(f - s1) * ratio);
+        path.add(PathPoint(f, target));
+    }
+
+    auto alignment = std::make_shared<AlignmentModel>(m_reference,
+                                                      m_toAlign,
+                                                      ModelId());
+
+    auto alignmentModelId = ModelById::add(alignment);
+
+    alignment->setPath(path);
+    alignment->setCompletion(100);
+    toAlign->setAlignment(alignmentModelId);
+    m_document->addNonDerivedModel(alignmentModelId);
+
+    emit complete(alignmentModelId);
+}
+
+bool
+LinearAligner::getTrimmedExtents(ModelId modelId,
+                                 sv_frame_t &start,
+                                 sv_frame_t &end)
+{
+    auto model = ModelById::getAs<DenseTimeValueModel>(modelId);
+    if (!model) return false;
+
+    sv_frame_t chunksize = 1024;
+    double threshold = 1e-2;
+
+    auto rms = [](const floatvec_t &samples) {
+                   double rms = 0.0;
+                   for (auto s: samples) {
+                       rms += s * s;
+                   }
+                   rms /= double(samples.size());
+                   rms = sqrt(rms);
+                   return rms;
+               };
+    
+    while (start < end) {
+        floatvec_t samples = model->getData(-1, start, chunksize);
+        if (samples.empty()) {
+            return false; // no non-silent content found
+        }
+        if (rms(samples) > threshold) {
+            for (auto s: samples) {
+                if (fabsf(s) > threshold) {
+                    break;
+                }
+                ++start;
+            }
+            break;
+        }
+        start += chunksize;
+    }
+    
+    if (start >= end) {
+        return false;
+    }
+
+    while (end > start) {
+        sv_frame_t probe = end - chunksize;
+        if (probe < 0) probe = 0;
+        floatvec_t samples = model->getData(-1, probe, chunksize);
+        if (samples.empty()) {
+            break;
+        }
+        if (rms(samples) > threshold) {
+            break;
+        }
+        end = probe;
+    }
+
+    return (end > start);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/LinearAligner.h	Fri Jun 26 13:48:52 2020 +0100
@@ -0,0 +1,50 @@
+/* -*- 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_LINEAR_ALIGNER_H
+#define SV_LINEAR_ALIGNER_H
+
+#include "Aligner.h"
+
+class AlignmentModel;
+class Document;
+
+class LinearAligner : public Aligner
+{
+    Q_OBJECT
+
+public:
+    LinearAligner(Document *doc,
+                  ModelId reference,
+                  ModelId toAlign,
+                  bool trimmed);
+
+    ~LinearAligner();
+
+    void begin() override;
+
+    static bool isAvailable() {
+        return true;
+    }
+
+private:
+    Document *m_document;
+    ModelId m_reference;
+    ModelId m_toAlign;
+    bool m_trimmed;
+
+    bool getTrimmedExtents(ModelId model, sv_frame_t &start, sv_frame_t &end);
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/MATCHAligner.cpp	Fri Jun 26 13:48:52 2020 +0100
@@ -0,0 +1,395 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "MATCHAligner.h"
+
+#include "data/model/SparseTimeValueModel.h"
+#include "data/model/RangeSummarisableTimeValueModel.h"
+#include "data/model/AlignmentModel.h"
+#include "data/model/AggregateWaveModel.h"
+
+#include "framework/Document.h"
+
+#include "transform/TransformFactory.h"
+#include "transform/ModelTransformerFactory.h"
+#include "transform/FeatureExtractionModelTransformer.h"
+
+#include <QSettings>
+
+MATCHAligner::MATCHAligner(Document *doc,
+                           ModelId reference,
+                           ModelId toAlign,
+                           bool withTuningDifference) :
+    m_document(doc),
+    m_reference(reference),
+    m_toAlign(toAlign),
+    m_withTuningDifference(withTuningDifference),
+    m_tuningFrequency(440.f),
+    m_incomplete(true)
+{
+}
+
+MATCHAligner::~MATCHAligner()
+{
+    if (m_incomplete) {
+        auto other =
+            ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
+        if (other) {
+            other->setAlignment({});
+        }
+    }
+
+    ModelById::release(m_tuningDiffOutputModel);
+    ModelById::release(m_pathOutputModel);
+}
+
+QString
+MATCHAligner::getAlignmentTransformName()
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    TransformId id = settings.value
+        ("transform-id",
+         "vamp:match-vamp-plugin:match:path").toString();
+    settings.endGroup();
+    return id;
+}
+
+QString
+MATCHAligner::getTuningDifferenceTransformName()
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+    TransformId id = settings.value
+        ("tuning-difference-transform-id",
+         "vamp:tuning-difference:tuning-difference:tuningfreq")
+        .toString();
+    settings.endGroup();
+    return id;
+}
+
+bool
+MATCHAligner::isAvailable()
+{
+    TransformFactory *factory = TransformFactory::getInstance();
+    TransformId id = getAlignmentTransformName();
+    TransformId tdId = getTuningDifferenceTransformName();
+    return factory->haveTransform(id) &&
+        (tdId == "" || factory->haveTransform(tdId));
+}
+
+void
+MATCHAligner::begin()
+{
+    auto reference =
+        ModelById::getAs<RangeSummarisableTimeValueModel>(m_reference);
+    auto other =
+        ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
+
+    if (!reference || !other) return;
+
+    // This involves creating a number of 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. We just call this one aggregateModel
+    //
+    // 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). This is m_tuningDiffOutputModel.
+    //
+    // 2b. a SparseTimeValueModel which will be automatically created
+    // by FeatureExtractionPluginTransformer when running the MATCH
+    // plugin to perform alignment (so containing the alignment path).
+    // This is m_pathOutputModel.
+    //
+    // 3. an AlignmentModel, which stores the path and carries out
+    // alignment lookups on it. This one is m_alignmentModel.
+    //
+    // Models 1 and 3 are registered with the document, which will
+    // eventually release them. We don't release them here except in
+    // the case where an activity fails before the point where we
+    // would otherwise have registered them with the document.
+    //
+    // Models 2a (m_tuningDiffOutputModel) and 2b (m_pathOutputModel)
+    // are not registered with the document, because they are not
+    // intended to persist. These have to be released by us when
+    // finished with, but their lifespans do not extend beyond the end
+    // of the alignment procedure, so this should be ok.
+
+    AggregateWaveModel::ChannelSpecList components;
+    components.push_back
+        (AggregateWaveModel::ModelChannelSpec(m_reference, -1));
+
+    components.push_back
+        (AggregateWaveModel::ModelChannelSpec(m_toAlign, -1));
+
+    auto aggregateModel = std::make_shared<AggregateWaveModel>(components);
+    m_aggregateModel = ModelById::add(aggregateModel);
+    m_document->addNonDerivedModel(m_aggregateModel);
+
+    auto alignmentModel = std::make_shared<AlignmentModel>
+        (m_reference, m_toAlign, ModelId());
+    m_alignmentModel = ModelById::add(alignmentModel);
+
+    TransformId tdId;
+    if (m_withTuningDifference) {
+        tdId = getTuningDifferenceTransformName();
+    }
+
+    if (tdId == "") {
+        
+        if (beginAlignmentPhase()) {
+            other->setAlignment(m_alignmentModel);
+            m_document->addNonDerivedModel(m_alignmentModel);
+        } else {
+            QString error = alignmentModel->getError();
+            ModelById::release(alignmentModel);
+            emit failed(m_toAlign, error);
+            return;
+        }
+
+    } 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", 60);
+        transform.setParameter("maxrange", 6);
+        transform.setParameter("finetuning", false);
+    
+        SVDEBUG << "MATCHAligner: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
+
+        ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
+
+        QString message;
+        m_tuningDiffOutputModel = mtf->transform(transform,
+                                                 m_aggregateModel,
+                                                 message);
+
+        auto tuningDiffOutputModel =
+            ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
+        if (!tuningDiffOutputModel) {
+            SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
+            ModelById::release(alignmentModel);
+            emit failed(m_toAlign, message);
+            return;
+        }
+
+        other->setAlignment(m_alignmentModel);
+        m_document->addNonDerivedModel(m_alignmentModel);
+    
+        connect(tuningDiffOutputModel.get(),
+                SIGNAL(completionChanged(ModelId)),
+                this, SLOT(tuningDifferenceCompletionChanged(ModelId)));
+    }
+}
+
+void
+MATCHAligner::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId)
+{
+    if (m_tuningDiffOutputModel.isNone()) {
+        // we're done, this is probably a spurious queued event
+        return;
+    }
+        
+    if (tuningDiffOutputModelId != m_tuningDiffOutputModel) {
+        SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged: Model "
+               << tuningDiffOutputModelId
+               << " is not ours! (ours is "
+               << m_tuningDiffOutputModel << ")" << endl;
+        return;
+    }
+
+    auto tuningDiffOutputModel =
+        ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
+    if (!tuningDiffOutputModel) {
+        SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged: Model "
+               << tuningDiffOutputModelId
+               << " not known as SparseTimeValueModel" << endl;
+        return;
+    }
+
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    if (!alignmentModel) {
+        SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged:"
+               << "alignment model has disappeared" << endl;
+        return;
+    }
+    
+    int completion = 0;
+    bool done = tuningDiffOutputModel->isReady(&completion);
+
+    SVDEBUG << "MATCHAligner::tuningDifferenceCompletionChanged: model "
+            << m_tuningDiffOutputModel << ", completion = " << completion
+            << ", done = " << done << endl;
+    
+    if (!done) {
+        // This will be the completion the alignment model reports,
+        // before the alignment actually begins
+        alignmentModel->setCompletion(completion / 2);
+        return;
+    }
+
+    m_tuningFrequency = 440.f;
+    
+    if (!tuningDiffOutputModel->isEmpty()) {
+        m_tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue();
+        SVCERR << "MATCHAligner::tuningDifferenceCompletionChanged: Reported tuning frequency = " << m_tuningFrequency << endl;
+    } else {
+        SVCERR << "MATCHAligner::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
+    }    
+    
+    ModelById::release(tuningDiffOutputModel);
+    m_tuningDiffOutputModel = {};
+    
+    beginAlignmentPhase();
+}
+
+bool
+MATCHAligner::beginAlignmentPhase()
+{
+    TransformId id = getAlignmentTransformName();
+    
+    SVDEBUG << "MATCHAligner::beginAlignmentPhase: transform is "
+            << id << endl;
+    
+    TransformFactory *tf = TransformFactory::getInstance();
+
+    auto aggregateModel =
+        ModelById::getAs<AggregateWaveModel>(m_aggregateModel);
+    auto alignmentModel =
+        ModelById::getAs<AlignmentModel>(m_alignmentModel);
+
+    if (!aggregateModel || !alignmentModel) {
+        SVCERR << "MATCHAligner::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl;
+        return false;
+    }
+    
+    Transform transform = tf->getDefaultTransformFor
+        (id, aggregateModel->getSampleRate());
+
+    transform.setStepSize(transform.getBlockSize()/2);
+    transform.setParameter("serialise", 1);
+    transform.setParameter("smooth", 0);
+    transform.setParameter("zonewidth", 40);
+    transform.setParameter("noise", true);
+    transform.setParameter("minfreq", 500);
+
+    int cents = 0;
+    
+    if (m_tuningFrequency != 0.f) {
+        transform.setParameter("freq2", m_tuningFrequency);
+
+        double centsOffset = 0.f;
+        int pitch = Pitch::getPitchForFrequency(m_tuningFrequency,
+                                                &centsOffset);
+        cents = int(round((pitch - 69) * 100 + centsOffset));
+        SVCERR << "MATCHAligner: frequency " << m_tuningFrequency
+               << " yields cents offset " << centsOffset
+               << " and pitch " << pitch << " -> cents " << cents << endl;
+    }
+
+    alignmentModel->setRelativePitch(cents);
+    
+    SVDEBUG << "MATCHAligner: Alignment transform step size "
+            << transform.getStepSize() << ", block size "
+            << transform.getBlockSize() << endl;
+
+    ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
+
+    QString message;
+    m_pathOutputModel = mtf->transform
+        (transform, m_aggregateModel, message);
+
+    if (m_pathOutputModel.isNone()) {
+        transform.setStepSize(0);
+        m_pathOutputModel = mtf->transform
+            (transform, m_aggregateModel, message);
+    }
+
+    auto pathOutputModel =
+        ModelById::getAs<SparseTimeValueModel>(m_pathOutputModel);
+
+    //!!! callers will need to be updated to get error from
+    //!!! alignment model after initial call
+        
+    if (!pathOutputModel) {
+        SVCERR << "MATCHAligner: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
+        alignmentModel->setError(message);
+        return false;
+    }
+
+    pathOutputModel->setCompletion(0);
+    alignmentModel->setPathFrom(m_pathOutputModel);
+
+    connect(pathOutputModel.get(), SIGNAL(completionChanged(ModelId)),
+            this, SLOT(alignmentCompletionChanged(ModelId)));
+
+    return true;
+}
+
+void
+MATCHAligner::alignmentCompletionChanged(ModelId pathOutputModelId)
+{
+    if (pathOutputModelId != m_pathOutputModel) {
+        SVCERR << "WARNING: MATCHAligner::alignmentCompletionChanged: Model "
+               << pathOutputModelId
+               << " is not ours! (ours is "
+               << m_pathOutputModel << ")" << endl;
+        return;
+    }
+
+    auto pathOutputModel =
+        ModelById::getAs<SparseTimeValueModel>(m_pathOutputModel);
+    if (!pathOutputModel) {
+        SVCERR << "WARNING: MATCHAligner::alignmentCompletionChanged: Path output model "
+               << m_pathOutputModel << " no longer exists" << endl;
+        return;
+    }
+        
+    int completion = 0;
+    bool done = pathOutputModel->isReady(&completion);
+
+    if (m_withTuningDifference) {
+        if (auto alignmentModel =
+            ModelById::getAs<AlignmentModel>(m_alignmentModel)) {
+            if (!done) {
+                int adjustedCompletion = 50 + completion/2;
+                if (adjustedCompletion > 99) {
+                    adjustedCompletion = 99;
+                }
+                alignmentModel->setCompletion(adjustedCompletion);
+            } else {
+                alignmentModel->setCompletion(100);
+            }
+        }
+    }
+
+    if (done) {
+        m_incomplete = false;
+        
+        ModelById::release(m_pathOutputModel);
+        m_pathOutputModel = {};
+
+        emit complete(m_alignmentModel);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/MATCHAligner.h	Fri Jun 26 13:48:52 2020 +0100
@@ -0,0 +1,62 @@
+/* -*- 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_MATCH_ALIGNER_H
+#define SV_MATCH_ALIGNER_H
+
+#include "Aligner.h"
+
+class AlignmentModel;
+class Document;
+
+class MATCHAligner : public Aligner
+{
+    Q_OBJECT
+
+public:
+    MATCHAligner(Document *doc,
+                 ModelId reference,
+                 ModelId toAlign,
+                 bool withTuningDifference);
+
+    // Destroy the aligner, cleanly cancelling any ongoing alignment
+    ~MATCHAligner();
+
+    void begin() override;
+
+    static bool isAvailable();
+
+private slots:
+    void alignmentCompletionChanged(ModelId);
+    void tuningDifferenceCompletionChanged(ModelId);
+
+private:
+    static QString getAlignmentTransformName();
+    static QString getTuningDifferenceTransformName();
+
+    bool beginAlignmentPhase();
+    
+    Document *m_document;
+    ModelId m_reference;
+    ModelId m_toAlign;
+    ModelId m_aggregateModel; // an AggregateWaveModel
+    ModelId m_alignmentModel; // an AlignmentModel
+    ModelId m_tuningDiffOutputModel; // SparseTimeValueModel, unreg'd with doc
+    ModelId m_pathOutputModel; // SparseTimeValueModel, unreg'd with doc
+    bool m_withTuningDifference;
+    float m_tuningFrequency;
+    bool m_incomplete;
+};
+
+#endif
--- a/align/TransformAligner.cpp	Wed Jun 03 13:58:29 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,404 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    
-    This program is free software; you can redistribute it and/or
-    modify it under the terms of the GNU General Public License as
-    published by the Free Software Foundation; either version 2 of the
-    License, or (at your option) any later version.  See the file
-    COPYING included with this distribution for more information.
-*/
-
-#include "TransformAligner.h"
-
-#include "data/model/SparseTimeValueModel.h"
-#include "data/model/RangeSummarisableTimeValueModel.h"
-#include "data/model/AlignmentModel.h"
-#include "data/model/AggregateWaveModel.h"
-
-#include "framework/Document.h"
-
-#include "transform/TransformFactory.h"
-#include "transform/ModelTransformerFactory.h"
-#include "transform/FeatureExtractionModelTransformer.h"
-
-#include <QSettings>
-
-TransformAligner::TransformAligner(Document *doc,
-                                   ModelId reference,
-                                   ModelId toAlign) :
-    m_document(doc),
-    m_reference(reference),
-    m_toAlign(toAlign),
-    m_tuningFrequency(440.f),
-    m_incomplete(true)
-{
-}
-
-TransformAligner::~TransformAligner()
-{
-    if (m_incomplete) {
-        auto other =
-            ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
-        if (other) {
-            other->setAlignment({});
-        }
-    }
-
-    ModelById::release(m_tuningDiffProgressModel);
-    ModelById::release(m_tuningDiffOutputModel);
-    ModelById::release(m_pathOutputModel);
-}
-
-QString
-TransformAligner::getAlignmentTransformName()
-{
-    QSettings settings;
-    settings.beginGroup("Alignment");
-    TransformId id =
-        settings.value("transform-id",
-                       "vamp:match-vamp-plugin:match:path").toString();
-    settings.endGroup();
-    return id;
-}
-
-QString
-TransformAligner::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
-TransformAligner::isAvailable()
-{
-    TransformFactory *factory = TransformFactory::getInstance();
-    TransformId id = getAlignmentTransformName();
-    TransformId tdId = getTuningDifferenceTransformName();
-    return factory->haveTransform(id) &&
-        (tdId == "" || factory->haveTransform(tdId));
-}
-
-void
-TransformAligner::begin()
-{
-    auto reference =
-        ModelById::getAs<RangeSummarisableTimeValueModel>(m_reference);
-    auto other =
-        ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
-
-    if (!reference || !other) return;
-
-    // This involves creating a number of 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. We just call this one aggregateModel
-    //
-    // 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). This is m_tuningDiffOutputModel.
-    //
-    // 2b. a SparseTimeValueModel which will be automatically created
-    // by FeatureExtractionPluginTransformer when running the MATCH
-    // plugin to perform alignment (so containing the alignment path).
-    // This is m_pathOutputModel.
-    //
-    // 2c. a SparseTimeValueModel used solely to provide faked
-    // completion information to the AlignmentModel while a
-    // TuningDifference calculation is going on. We call this
-    // m_tuningDiffProgressModel.
-    //
-    // 3. an AlignmentModel, which stores the path and carries out
-    // alignment lookups on it. This one is m_alignmentModel.
-    //
-    // Models 1 and 3 are registered with the document, which will
-    // eventually release them. We don't release them here except in
-    // the case where an activity fails before the point where we
-    // would otherwise have registered them with the document.
-    //
-    // Models 2a (m_tuningDiffOutputModel), 2b (m_pathOutputModel) and
-    // 2c (m_tuningDiffProgressModel) are not registered with the
-    // document, because they are not intended to persist, and also
-    // Model 2c (m_tuningDiffProgressModel) is a bodge that we are
-    // embarrassed about, so we try to manage it ourselves without
-    // anyone else noticing. These have to be released by us when
-    // finished with, but their lifespans do not extend beyond the end
-    // of the alignment procedure, so this should be ok.
-
-    AggregateWaveModel::ChannelSpecList components;
-    components.push_back
-        (AggregateWaveModel::ModelChannelSpec(m_reference, -1));
-
-    components.push_back
-        (AggregateWaveModel::ModelChannelSpec(m_toAlign, -1));
-
-    auto aggregateModel = std::make_shared<AggregateWaveModel>(components);
-    m_aggregateModel = ModelById::add(aggregateModel);
-    m_document->addNonDerivedModel(m_aggregateModel);
-
-    auto alignmentModel = std::make_shared<AlignmentModel>
-        (m_reference, m_toAlign, ModelId());
-    m_alignmentModel = ModelById::add(alignmentModel);
-
-    TransformId tdId = getTuningDifferenceTransformName();
-
-    if (tdId == "") {
-        
-        if (beginAlignmentPhase()) {
-            other->setAlignment(m_alignmentModel);
-            m_document->addNonDerivedModel(m_alignmentModel);
-        } else {
-            QString error = alignmentModel->getError();
-            ModelById::release(alignmentModel);
-            emit failed(m_toAlign, error);
-            return;
-        }
-
-    } 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", 60);
-        transform.setParameter("maxrange", 6);
-        transform.setParameter("finetuning", false);
-    
-        SVDEBUG << "TransformAligner: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
-
-        ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
-
-        QString message;
-        m_tuningDiffOutputModel = mtf->transform(transform,
-                                                 m_aggregateModel,
-                                                 message);
-
-        auto tuningDiffOutputModel =
-            ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
-        if (!tuningDiffOutputModel) {
-            SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
-            ModelById::release(alignmentModel);
-            emit failed(m_toAlign, message);
-            return;
-        }
-
-        other->setAlignment(m_alignmentModel);
-        m_document->addNonDerivedModel(m_alignmentModel);
-    
-        connect(tuningDiffOutputModel.get(),
-                SIGNAL(completionChanged(ModelId)),
-                this, SLOT(tuningDifferenceCompletionChanged(ModelId)));
-
-        // This model exists only so that the AlignmentModel can get a
-        // completion value from somewhere while the tuning difference
-        // calculation is going on
-        auto progressModel = std::make_shared<SparseTimeValueModel>
-            (aggregateModel->getSampleRate(), 1);
-        m_tuningDiffProgressModel = ModelById::add(progressModel);
-        progressModel->setCompletion(0);
-        alignmentModel->setPathFrom(m_tuningDiffProgressModel);
-    }
-}
-
-void
-TransformAligner::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId)
-{
-    if (m_tuningDiffOutputModel.isNone()) {
-        // we're done, this is probably a spurious queued event
-        return;
-    }
-        
-    if (tuningDiffOutputModelId != m_tuningDiffOutputModel) {
-        SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged: Model "
-               << tuningDiffOutputModelId
-               << " is not ours! (ours is "
-               << m_tuningDiffOutputModel << ")" << endl;
-        return;
-    }
-
-    auto tuningDiffOutputModel =
-        ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
-    if (!tuningDiffOutputModel) {
-        SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged: Model "
-               << tuningDiffOutputModelId
-               << " not known as SparseTimeValueModel" << endl;
-        return;
-    }
-
-    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
-    if (!alignmentModel) {
-        SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged:"
-               << "alignment model has disappeared" << endl;
-        return;
-    }
-    
-    int completion = 0;
-    bool done = tuningDiffOutputModel->isReady(&completion);
-
-    SVDEBUG << "TransformAligner::tuningDifferenceCompletionChanged: model "
-            << m_tuningDiffOutputModel << ", completion = " << completion
-            << ", done = " << done << 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);
-        auto progressModel =
-            ModelById::getAs<SparseTimeValueModel>(m_tuningDiffProgressModel);
-        if (progressModel) {
-            progressModel->setCompletion(clamped);
-        }
-        return;
-    }
-
-    m_tuningFrequency = 440.f;
-    
-    if (!tuningDiffOutputModel->isEmpty()) {
-        m_tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue();
-        SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: Reported tuning frequency = " << m_tuningFrequency << endl;
-    } else {
-        SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
-    }    
-    
-    ModelById::release(tuningDiffOutputModel);
-    m_tuningDiffOutputModel = {};
-    
-    alignmentModel->setPathFrom({}); // replace m_tuningDiffProgressModel
-    ModelById::release(m_tuningDiffProgressModel);
-    m_tuningDiffProgressModel = {};
-
-    beginAlignmentPhase();
-}
-
-bool
-TransformAligner::beginAlignmentPhase()
-{
-    TransformId id = getAlignmentTransformName();
-    
-    SVDEBUG << "TransformAligner::beginAlignmentPhase: transform is "
-            << id << endl;
-    
-    TransformFactory *tf = TransformFactory::getInstance();
-
-    auto aggregateModel =
-        ModelById::getAs<AggregateWaveModel>(m_aggregateModel);
-    auto alignmentModel =
-        ModelById::getAs<AlignmentModel>(m_alignmentModel);
-
-    if (!aggregateModel || !alignmentModel) {
-        SVCERR << "TransformAligner::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl;
-        return false;
-    }
-    
-    Transform transform = tf->getDefaultTransformFor
-        (id, aggregateModel->getSampleRate());
-
-    transform.setStepSize(transform.getBlockSize()/2);
-    transform.setParameter("serialise", 1);
-    transform.setParameter("smooth", 0);
-    transform.setParameter("zonewidth", 40);
-    transform.setParameter("noise", true);
-    transform.setParameter("minfreq", 500);
-
-    int cents = 0;
-    
-    if (m_tuningFrequency != 0.f) {
-        transform.setParameter("freq2", m_tuningFrequency);
-
-        double centsOffset = 0.f;
-        int pitch = Pitch::getPitchForFrequency(m_tuningFrequency,
-                                                &centsOffset);
-        cents = int(round((pitch - 69) * 100 + centsOffset));
-        SVCERR << "TransformAligner: frequency " << m_tuningFrequency
-               << " yields cents offset " << centsOffset
-               << " and pitch " << pitch << " -> cents " << cents << endl;
-    }
-
-    alignmentModel->setRelativePitch(cents);
-    
-    SVDEBUG << "TransformAligner: Alignment transform step size "
-            << transform.getStepSize() << ", block size "
-            << transform.getBlockSize() << endl;
-
-    ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
-
-    QString message;
-    m_pathOutputModel = mtf->transform
-        (transform, m_aggregateModel, message);
-
-    if (m_pathOutputModel.isNone()) {
-        transform.setStepSize(0);
-        m_pathOutputModel = mtf->transform
-            (transform, m_aggregateModel, message);
-    }
-
-    auto pathOutputModel =
-        ModelById::getAs<SparseTimeValueModel>(m_pathOutputModel);
-
-    //!!! callers will need to be updated to get error from
-    //!!! alignment model after initial call
-        
-    if (!pathOutputModel) {
-        SVCERR << "TransformAligner: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
-        alignmentModel->setError(message);
-        return false;
-    }
-
-    pathOutputModel->setCompletion(0);
-    alignmentModel->setPathFrom(m_pathOutputModel);
-
-    connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)),
-            this, SLOT(alignmentCompletionChanged(ModelId)));
-
-    return true;
-}
-
-void
-TransformAligner::alignmentCompletionChanged(ModelId alignmentModelId)
-{
-    if (alignmentModelId != m_alignmentModel) {
-        SVCERR << "WARNING: TransformAligner::alignmentCompletionChanged: Model "
-               << alignmentModelId
-               << " is not ours! (ours is "
-               << m_alignmentModel << ")" << endl;
-        return;
-    }
-
-    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
-
-    if (alignmentModel && alignmentModel->isReady()) {
-
-        m_incomplete = false;
-        
-        ModelById::release(m_pathOutputModel);
-        m_pathOutputModel = {};
-
-        disconnect(alignmentModel.get(),
-                   SIGNAL(completionChanged(ModelId)),
-                   this, SLOT(alignmentCompletionChanged(ModelId)));
-        emit complete(m_alignmentModel);
-    }
-}
--- a/align/TransformAligner.h	Wed Jun 03 13:58:29 2020 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
-
-/*
-    Sonic Visualiser
-    An audio file viewer and annotation editor.
-    Centre for Digital Music, Queen Mary, University of London.
-    
-    This 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_TRANSFORM_ALIGNER_H
-#define SV_TRANSFORM_ALIGNER_H
-
-#include "Aligner.h"
-
-class AlignmentModel;
-class Document;
-
-class TransformAligner : public Aligner
-{
-    Q_OBJECT
-
-public:
-    TransformAligner(Document *doc,
-                     ModelId reference,
-                     ModelId toAlign);
-
-    // Destroy the aligner, cleanly cancelling any ongoing alignment
-    ~TransformAligner();
-
-    void begin() override;
-
-    static bool isAvailable();
-
-private slots:
-    void alignmentCompletionChanged(ModelId);
-    void tuningDifferenceCompletionChanged(ModelId);
-
-private:
-    static QString getAlignmentTransformName();
-    static QString getTuningDifferenceTransformName();
-
-    bool beginAlignmentPhase();
-    
-    Document *m_document;
-    ModelId m_reference;
-    ModelId m_toAlign;
-    ModelId m_aggregateModel; // an AggregateWaveModel
-    ModelId m_alignmentModel; // an AlignmentModel
-    ModelId m_tuningDiffProgressModel; // SparseTimeValueModel, unreg'd with doc
-    ModelId m_tuningDiffOutputModel; // SparseTimeValueModel, unreg'd with doc
-    ModelId m_pathOutputModel; // SparseTimeValueModel, unreg'd with doc
-    float m_tuningFrequency;
-    bool m_incomplete;
-};
-
-#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/TransformDTWAligner.cpp	Fri Jun 26 13:48:52 2020 +0100
@@ -0,0 +1,478 @@
+/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    Sonic Visualiser
+    An audio file viewer and annotation editor.
+    Centre for Digital Music, Queen Mary, University of London.
+    
+    This program is free software; you can redistribute it and/or
+    modify it under the terms of the GNU General Public License as
+    published by the Free Software Foundation; either version 2 of the
+    License, or (at your option) any later version.  See the file
+    COPYING included with this distribution for more information.
+*/
+
+#include "TransformDTWAligner.h"
+#include "DTW.h"
+
+#include "data/model/SparseTimeValueModel.h"
+#include "data/model/NoteModel.h"
+#include "data/model/RangeSummarisableTimeValueModel.h"
+#include "data/model/AlignmentModel.h"
+#include "data/model/AggregateWaveModel.h"
+
+#include "framework/Document.h"
+
+#include "transform/ModelTransformerFactory.h"
+#include "transform/FeatureExtractionModelTransformer.h"
+
+#include <QSettings>
+#include <QMutex>
+#include <QMutexLocker>
+
+using std::vector;
+
+static
+TransformDTWAligner::MagnitudePreprocessor identityMagnitudePreprocessor =
+    [](double x) {
+        return x;
+    };
+
+static
+TransformDTWAligner::RiseFallPreprocessor identityRiseFallPreprocessor =
+    [](double prev, double curr) {
+        if (prev == curr) {
+            return RiseFallDTW::Value({ RiseFallDTW::Direction::None, 0.0 });
+        } else if (curr > prev) {
+            return RiseFallDTW::Value({ RiseFallDTW::Direction::Up, curr - prev });
+        } else {
+            return RiseFallDTW::Value({ RiseFallDTW::Direction::Down, prev - curr });
+        }
+    };
+
+QMutex
+TransformDTWAligner::m_dtwMutex;
+
+TransformDTWAligner::TransformDTWAligner(Document *doc,
+                                         ModelId reference,
+                                         ModelId toAlign,
+                                         Transform transform,
+                                         DTWType dtwType) :
+    m_document(doc),
+    m_reference(reference),
+    m_toAlign(toAlign),
+    m_transform(transform),
+    m_dtwType(dtwType),
+    m_incomplete(true),
+    m_magnitudePreprocessor(identityMagnitudePreprocessor),
+    m_riseFallPreprocessor(identityRiseFallPreprocessor)
+{
+}
+
+TransformDTWAligner::TransformDTWAligner(Document *doc,
+                                         ModelId reference,
+                                         ModelId toAlign,
+                                         Transform transform,
+                                         MagnitudePreprocessor outputPreprocessor) :
+    m_document(doc),
+    m_reference(reference),
+    m_toAlign(toAlign),
+    m_transform(transform),
+    m_dtwType(Magnitude),
+    m_incomplete(true),
+    m_magnitudePreprocessor(outputPreprocessor),
+    m_riseFallPreprocessor(identityRiseFallPreprocessor)
+{
+}
+
+TransformDTWAligner::TransformDTWAligner(Document *doc,
+                                         ModelId reference,
+                                         ModelId toAlign,
+                                         Transform transform,
+                                         RiseFallPreprocessor outputPreprocessor) :
+    m_document(doc),
+    m_reference(reference),
+    m_toAlign(toAlign),
+    m_transform(transform),
+    m_dtwType(RiseFall),
+    m_incomplete(true),
+    m_magnitudePreprocessor(identityMagnitudePreprocessor),
+    m_riseFallPreprocessor(outputPreprocessor)
+{
+}
+
+TransformDTWAligner::~TransformDTWAligner()
+{
+    if (m_incomplete) {
+        if (auto toAlign = ModelById::get(m_toAlign)) {
+            toAlign->setAlignment({});
+        }
+    }
+    
+    ModelById::release(m_referenceOutputModel);
+    ModelById::release(m_toAlignOutputModel);
+}
+
+bool
+TransformDTWAligner::isAvailable()
+{
+    //!!! needs to be isAvailable(QString transformId)?
+    return true;
+}
+
+void
+TransformDTWAligner::begin()
+{
+    auto reference =
+        ModelById::getAs<RangeSummarisableTimeValueModel>(m_reference);
+    auto toAlign =
+        ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
+
+    if (!reference || !toAlign) return;
+
+    SVCERR << "TransformDTWAligner[" << this << "]: begin(): aligning "
+           << m_toAlign << " against reference " << m_reference << endl;
+    
+    ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
+
+    QString message;
+
+    m_referenceOutputModel = mtf->transform(m_transform, m_reference, message);
+    auto referenceOutputModel = ModelById::get(m_referenceOutputModel);
+    if (!referenceOutputModel) {
+        SVCERR << "Align::alignModel: ERROR: Failed to create reference output model (no plugin?)" << endl;
+        emit failed(m_toAlign, message);
+        return;
+    }
+
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+    SVCERR << "TransformDTWAligner[" << this << "]: begin(): transform id "
+           << m_transform.getIdentifier()
+           << " is running on reference model" << endl;
+#endif
+
+    message = "";
+
+    m_toAlignOutputModel = mtf->transform(m_transform, m_toAlign, message);
+    auto toAlignOutputModel = ModelById::get(m_toAlignOutputModel);
+    if (!toAlignOutputModel) {
+        SVCERR << "Align::alignModel: ERROR: Failed to create toAlign output model (no plugin?)" << endl;
+        emit failed(m_toAlign, message);
+        return;
+    }
+
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+    SVCERR << "TransformDTWAligner[" << this << "]: begin(): transform id "
+           << m_transform.getIdentifier()
+           << " is running on toAlign model" << endl;
+#endif
+
+    connect(referenceOutputModel.get(), SIGNAL(completionChanged(ModelId)),
+            this, SLOT(completionChanged(ModelId)));
+    connect(toAlignOutputModel.get(), SIGNAL(completionChanged(ModelId)),
+            this, SLOT(completionChanged(ModelId)));
+    
+    auto alignmentModel = std::make_shared<AlignmentModel>
+        (m_reference, m_toAlign, ModelId());
+    m_alignmentModel = ModelById::add(alignmentModel);
+    
+    toAlign->setAlignment(m_alignmentModel);
+    m_document->addNonDerivedModel(m_alignmentModel);
+
+    // we wouldn't normally expect these to be true here, but...
+    int completion = 0;
+    if (referenceOutputModel->isReady(&completion) &&
+        toAlignOutputModel->isReady(&completion)) {
+        SVCERR << "TransformDTWAligner[" << this << "]: begin(): output models "
+               << "are ready already! calling performAlignment" << endl;
+        if (performAlignment()) {
+            emit complete(m_alignmentModel);
+        } else {
+            emit failed(m_toAlign, tr("Failed to calculate alignment using DTW"));
+        }
+    }
+}
+
+void
+TransformDTWAligner::completionChanged(ModelId id)
+{
+    if (!m_incomplete) {
+        return;
+    }
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+    SVCERR << "TransformDTWAligner[" << this << "]: completionChanged: "
+           << "model " << id << endl;
+#endif
+    
+    auto referenceOutputModel = ModelById::get(m_referenceOutputModel);
+    auto toAlignOutputModel = ModelById::get(m_toAlignOutputModel);
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+
+    if (!referenceOutputModel || !toAlignOutputModel || !alignmentModel) {
+        return;
+    }
+
+    int referenceCompletion = 0, toAlignCompletion = 0;
+    bool referenceReady = referenceOutputModel->isReady(&referenceCompletion);
+    bool toAlignReady = toAlignOutputModel->isReady(&toAlignCompletion);
+
+    if (referenceReady && toAlignReady) {
+
+        SVCERR << "TransformDTWAligner[" << this << "]: completionChanged: "
+               << "both models ready, calling performAlignment" << endl;
+
+        alignmentModel->setCompletion(95);
+        
+        if (performAlignment()) {
+            emit complete(m_alignmentModel);
+        } else {
+            emit failed(m_toAlign, tr("Alignment of transform outputs failed"));
+        }
+
+    } else {
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+        SVCERR << "TransformDTWAligner[" << this << "]: completionChanged: "
+               << "not ready yet: reference completion " << referenceCompletion
+               << ", toAlign completion " << toAlignCompletion << endl;
+#endif
+        
+        int completion = std::min(referenceCompletion,
+                                  toAlignCompletion);
+        completion = (completion * 94) / 100;
+        alignmentModel->setCompletion(completion);
+    }
+}
+
+bool
+TransformDTWAligner::performAlignment()
+{
+    if (m_dtwType == Magnitude) {
+        return performAlignmentMagnitude();
+    } else {
+        return performAlignmentRiseFall();
+    }
+}
+
+bool
+TransformDTWAligner::getValuesFrom(ModelId modelId,
+                                   vector<sv_frame_t> &frames,
+                                   vector<double> &values,
+                                   sv_frame_t &resolution)
+{
+    EventVector events;
+
+    if (auto model = ModelById::getAs<SparseTimeValueModel>(modelId)) {
+        resolution = model->getResolution();
+        events = model->getAllEvents();
+    } else if (auto model = ModelById::getAs<NoteModel>(modelId)) {
+        resolution = model->getResolution();
+        events = model->getAllEvents();
+    } else {
+        SVCERR << "TransformDTWAligner::getValuesFrom: Type of model "
+               << modelId << " is not supported" << endl;
+        return false;
+    }
+
+    frames.clear();
+    values.clear();
+
+    for (auto e: events) {
+        frames.push_back(e.getFrame());
+        values.push_back(e.getValue());
+    }
+
+    return true;
+}
+
+Path
+TransformDTWAligner::makePath(const vector<size_t> &alignment,
+                              const vector<sv_frame_t> &refFrames,
+                              const vector<sv_frame_t> &otherFrames,
+                              sv_samplerate_t sampleRate,
+                              sv_frame_t resolution)
+{
+    Path path(sampleRate, resolution);
+
+    path.add(PathPoint(0, 0));
+    
+    for (int i = 0; in_range_for(alignment, i); ++i) {
+
+        // DTW returns "the index into s2 for each element in s1"
+        sv_frame_t refFrame = refFrames[i];
+
+        if (!in_range_for(otherFrames, alignment[i])) {
+            SVCERR << "TransformDTWAligner::makePath: Internal error: "
+                   << "DTW maps index " << i << " in reference frame vector "
+                   << "(size " << refFrames.size() << ") onto index "
+                   << alignment[i] << " in other frame vector "
+                   << "(only size " << otherFrames.size() << ")" << endl;
+            continue;
+        }
+            
+        sv_frame_t alignedFrame = otherFrames[alignment[i]];
+        path.add(PathPoint(alignedFrame, refFrame));
+    }
+
+    return path;
+}
+
+bool
+TransformDTWAligner::performAlignmentMagnitude()
+{
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    if (!alignmentModel) {
+        return false;
+    }
+
+    vector<sv_frame_t> refFrames, otherFrames;
+    vector<double> refValues, otherValues;
+    sv_frame_t resolution = 0;
+
+    if (!getValuesFrom(m_referenceOutputModel,
+                       refFrames, refValues, resolution)) {
+        return false;
+    }
+
+    if (!getValuesFrom(m_toAlignOutputModel,
+                       otherFrames, otherValues, resolution)) {
+        return false;
+    }
+    
+    vector<double> s1, s2;
+    for (double v: refValues) {
+        s1.push_back(m_magnitudePreprocessor(v));
+    }
+    for (double v: otherValues) {
+        s2.push_back(m_magnitudePreprocessor(v));
+    }
+
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignmentMagnitude: "
+           << "Have " << s1.size() << " events from reference, "
+           << s2.size() << " from toAlign" << endl;
+#endif
+    
+    MagnitudeDTW dtw;
+    vector<size_t> alignment;
+
+    {
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+        SVCERR << "TransformDTWAligner[" << this
+               << "]: serialising DTW to avoid over-allocation" << endl;
+#endif
+        QMutexLocker locker(&m_dtwMutex);
+        alignment = dtw.alignSeries(s1, s2);
+    }
+
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignmentMagnitude: "
+           << "DTW produced " << alignment.size() << " points:" << endl;
+    for (int i = 0; in_range_for(alignment, i) && i < 100; ++i) {
+        SVCERR << alignment[i] << " ";
+    }
+    SVCERR << endl;
+#endif
+
+    alignmentModel->setPath(makePath(alignment,
+                                     refFrames,
+                                     otherFrames,
+                                     alignmentModel->getSampleRate(),
+                                     resolution));
+    alignmentModel->setCompletion(100);
+
+    SVCERR << "TransformDTWAligner[" << this
+           << "]: performAlignmentMagnitude: Done" << endl;
+
+    m_incomplete = false;
+    return true;
+}
+
+bool
+TransformDTWAligner::performAlignmentRiseFall()
+{
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    if (!alignmentModel) {
+        return false;
+    }
+
+    vector<sv_frame_t> refFrames, otherFrames;
+    vector<double> refValues, otherValues;
+    sv_frame_t resolution = 0;
+
+    if (!getValuesFrom(m_referenceOutputModel,
+                       refFrames, refValues, resolution)) {
+        return false;
+    }
+
+    if (!getValuesFrom(m_toAlignOutputModel,
+                       otherFrames, otherValues, resolution)) {
+        return false;
+    }
+    
+    auto preprocess =
+        [this](const std::vector<double> &vv) {
+            vector<RiseFallDTW::Value> s;
+            double prev = 0.0;
+            for (auto curr: vv) {
+                s.push_back(m_riseFallPreprocessor(prev, curr));
+                prev = curr;
+            }
+            return s;
+        }; 
+    
+    vector<RiseFallDTW::Value> s1 = preprocess(refValues);
+    vector<RiseFallDTW::Value> s2 = preprocess(otherValues);
+
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignmentRiseFall: "
+           << "Have " << s1.size() << " events from reference, "
+           << s2.size() << " from toAlign" << endl;
+
+    SVCERR << "Reference:" << endl;
+    for (int i = 0; in_range_for(s1, i) && i < 100; ++i) {
+        SVCERR << s1[i] << " ";
+    }
+    SVCERR << endl;
+
+    SVCERR << "toAlign:" << endl;
+    for (int i = 0; in_range_for(s2, i) && i < 100; ++i) {
+        SVCERR << s2[i] << " ";
+    }
+    SVCERR << endl;
+#endif
+    
+    RiseFallDTW dtw;
+    vector<size_t> alignment;
+
+    {
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+        SVCERR << "TransformDTWAligner[" << this
+               << "]: serialising DTW to avoid over-allocation" << endl;
+#endif
+        QMutexLocker locker(&m_dtwMutex);
+        alignment = dtw.alignSeries(s1, s2);
+    }
+
+#ifdef DEBUG_TRANSFORM_DTW_ALIGNER
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignmentRiseFall: "
+           << "DTW produced " << alignment.size() << " points:" << endl;
+    for (int i = 0; i < alignment.size() && i < 100; ++i) {
+        SVCERR << alignment[i] << " ";
+    }
+    SVCERR << endl;
+#endif
+
+    alignmentModel->setPath(makePath(alignment,
+                                     refFrames,
+                                     otherFrames,
+                                     alignmentModel->getSampleRate(),
+                                     resolution));
+
+    alignmentModel->setCompletion(100);
+
+    SVCERR << "TransformDTWAligner[" << this
+           << "]: performAlignmentRiseFall: Done" << endl;
+
+    m_incomplete = false;
+    return true;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/TransformDTWAligner.h	Fri Jun 26 13:48:52 2020 +0100
@@ -0,0 +1,122 @@
+/* -*- 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_TRANSFORM_DTW_ALIGNER_H
+#define SV_TRANSFORM_DTW_ALIGNER_H
+
+#include "Aligner.h"
+#include "DTW.h"
+
+#include "transform/Transform.h"
+#include "svcore/data/model/Path.h"
+
+#include <functional>
+
+#include <QMutex>
+
+class AlignmentModel;
+class Document;
+
+class TransformDTWAligner : public Aligner
+{
+    Q_OBJECT
+
+public:
+    enum DTWType {
+        Magnitude,
+        RiseFall
+    };
+
+    /**
+     * Create a TransformDTWAligner that runs the given transform on
+     * both models and feeds the resulting values into the given DTW
+     * type. If DTWType is Magnitude, the transform output values are
+     * used unmodified; if RiseFall, the deltas between consecutive
+     * values are used.
+     */
+    TransformDTWAligner(Document *doc,
+                        ModelId reference,
+                        ModelId toAlign,
+                        Transform transform,
+                        DTWType dtwType);
+
+    typedef std::function<double(double)> MagnitudePreprocessor;
+
+    /**
+     * Create a TransformDTWAligner that runs the given transform on
+     * both models, applies the supplied output preprocessor, and
+     * feeds the resulting values into a Magnitude DTW type.
+     */
+    TransformDTWAligner(Document *doc,
+                        ModelId reference,
+                        ModelId toAlign,
+                        Transform transform,
+                        MagnitudePreprocessor outputPreprocessor);
+
+    typedef std::function<RiseFallDTW::Value(double prev, double curr)>
+        RiseFallPreprocessor;
+
+    /**
+     * Create a TransformDTWAligner that runs the given transform on
+     * both models, applies the supplied output preprocessor, and
+     * feeds the resulting values into a RiseFall DTW type.
+     */
+    TransformDTWAligner(Document *doc,
+                        ModelId reference,
+                        ModelId toAlign,
+                        Transform transform,
+                        RiseFallPreprocessor outputPreprocessor);
+
+    // Destroy the aligner, cleanly cancelling any ongoing alignment
+    ~TransformDTWAligner();
+
+    void begin() override;
+
+    static bool isAvailable();
+
+private slots:
+    void completionChanged(ModelId);
+
+private:
+    bool performAlignment();
+    bool performAlignmentMagnitude();
+    bool performAlignmentRiseFall();
+
+    bool getValuesFrom(ModelId modelId,
+                       std::vector<sv_frame_t> &frames,
+                       std::vector<double> &values,
+                       sv_frame_t &resolution);
+
+    Path makePath(const std::vector<size_t> &alignment,
+                  const std::vector<sv_frame_t> &refFrames,
+                  const std::vector<sv_frame_t> &otherFrames,
+                  sv_samplerate_t sampleRate,
+                  sv_frame_t resolution);
+    
+    Document *m_document;
+    ModelId m_reference;
+    ModelId m_toAlign;
+    ModelId m_referenceOutputModel;
+    ModelId m_toAlignOutputModel;
+    ModelId m_alignmentModel;
+    Transform m_transform;
+    DTWType m_dtwType;
+    bool m_incomplete;
+    MagnitudePreprocessor m_magnitudePreprocessor;
+    RiseFallPreprocessor m_riseFallPreprocessor;
+
+    static QMutex m_dtwMutex;
+};
+
+#endif
--- a/files.pri	Wed Jun 03 13:58:29 2020 +0100
+++ b/files.pri	Fri Jun 26 13:48:52 2020 +0100
@@ -3,7 +3,9 @@
            align/Align.h \
            align/Aligner.h \
            align/ExternalProgramAligner.h \
-           align/TransformAligner.h \
+           align/LinearAligner.h \
+           align/MATCHAligner.h \
+           align/TransformDTWAligner.h \
            audio/AudioCallbackPlaySource.h \
            audio/AudioCallbackRecordTarget.h \
            audio/AudioGenerator.h \
@@ -22,7 +24,9 @@
 SVAPP_SOURCES += \
 	   align/Align.cpp \
            align/ExternalProgramAligner.cpp \
-           align/TransformAligner.cpp \
+           align/LinearAligner.cpp \
+           align/MATCHAligner.cpp \
+           align/TransformDTWAligner.cpp \
            audio/AudioCallbackPlaySource.cpp \
            audio/AudioCallbackRecordTarget.cpp \
            audio/AudioGenerator.cpp \
--- a/framework/Document.cpp	Wed Jun 03 13:58:29 2020 +0100
+++ b/framework/Document.cpp	Fri Jun 26 13:48:52 2020 +0100
@@ -1181,7 +1181,8 @@
 
     SVDEBUG << "Document::alignModel: aligning..." << endl;
     if (!rm->getAlignmentReference().isNone()) {
-        SVDEBUG << "(Note: model " << rm << " is currently aligned to model "
+        SVDEBUG << "(Note: model " << modelId
+                << " is currently aligned to model "
                 << rm->getAlignmentReference() << "; this will replace that)"
                 << endl;
     }