changeset 767:dd742e566e60 pitch-align

Make a start on further alignment methods
author Chris Cannam
date Thu, 21 May 2020 16:21:57 +0100
parents 6429a164b7e1
children 1b1960009be6
files align/Align.cpp align/Align.h align/LinearAligner.cpp align/LinearAligner.h align/TransformAligner.cpp align/TransformAligner.h align/TransformDTWAligner.cpp align/TransformDTWAligner.h files.pri
diffstat 9 files changed, 802 insertions(+), 48 deletions(-) [+]
line wrap: on
line diff
--- a/align/Align.cpp	Wed May 06 11:45:27 2020 +0100
+++ b/align/Align.cpp	Thu May 21 16:21:57 2020 +0100
@@ -13,20 +13,65 @@
 */
 
 #include "Align.h"
+
+#include "LinearAligner.h"
 #include "TransformAligner.h"
+#include "TransformDTWAligner.h"
 #include "ExternalProgramAligner.h"
+
 #include "framework/Document.h"
 
+#include "transform/Transform.h"
+#include "transform/TransformFactory.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 SungPitchContourAlignment:
+        return "sung-pitch-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,23 +79,24 @@
                          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);
+    QString additionalData;
+    AlignmentType type = getAlignmentPreference(additionalData);
     
     std::shared_ptr<Aligner> aligner;
 
@@ -60,21 +106,67 @@
         // 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<TransformAligner>(doc,
+                                                    reference,
+                                                    toAlign,
+                                                    withTuningDifference);
+            break;
+        }
+
+        case SungPitchContourAlignment:
+        {
+            auto refModel = ModelById::get(reference);
+            if (!refModel) return false;
+            
+            Transform transform = TransformFactory::getInstance()->
+                getDefaultTransformFor("vamp:pyin:pyin:smoothedpitchtrack",
+                                       refModel->getSampleRate());
+
+            transform.setParameter("outputunvoiced", 2.f);
+            
+            aligner = make_shared<TransformDTWAligner>
+                (doc,
+                 reference,
+                 toAlign,
+                 transform,
+                 TransformDTWAligner::RiseFall);
+            break;
+        }
+        
+        case TransformDrivenDTWAlignment:
+            throw std::logic_error("Not yet implemented"); //!!!
+
+        case ExternalProgramAlignment: {
+            aligner = make_shared<ExternalProgramAligner>(doc,
+                                                          reference,
+                                                          toAlign,
+                                                          additionalData);
+        }
+        }
+
+        m_aligners[toAlign] = aligner;
     }
 
     connect(aligner.get(), SIGNAL(complete(ModelId)),
@@ -82,27 +174,57 @@
 
     connect(aligner.get(), SIGNAL(failed(ModelId, QString)),
             this, SLOT(alignerFailed(ModelId, QString)));
+
+    return true;
+}
+
+Align::AlignmentType
+Align::getAlignmentPreference(QString &additionalData)
+{
+    QSettings settings;
+    settings.beginGroup("Alignment");
+
+    QString tag = settings.value
+        ("alignment-type", getAlignmentTypeTag(MATCHAlignment)).toString();
+
+    AlignmentType type = getAlignmentTypeForTag(tag);
+
+    if (type == TransformDrivenDTWAlignment) {
+        additionalData = settings.value("alignment-transform", "").toString();
+    } else if (type == ExternalProgramAlignment) {
+        additionalData = settings.value("alignment-program", "").toString();
+    }
+
+    settings.endGroup();
+    return type;
 }
 
 void
-Align::getAlignerPreference(bool &useProgram, QString &program)
+Align::setAlignmentPreference(AlignmentType type, QString additionalData)
 {
     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);
+
+    if (type == TransformDrivenDTWAlignment) {
+        settings.setValue("alignment-transform", additionalData);
+    } else if (type == ExternalProgramAlignment) {
+        settings.setValue("alignment-program", additionalData);
+    }
+
     settings.endGroup();
 }
 
 bool
 Align::canAlign() 
 {
-    bool useProgram;
-    QString program;
-    getAlignerPreference(useProgram, program);
+    QString additionalData;
+    AlignmentType type = getAlignmentPreference(additionalData);
 
-    if (useProgram) {
-        return ExternalProgramAligner::isAvailable(program);
+    if (type == ExternalProgramAlignment) {
+        return ExternalProgramAligner::isAvailable(additionalData);
     } else {
         return TransformAligner::isAvailable();
     }
--- a/align/Align.h	Wed May 06 11:45:27 2020 +0100
+++ b/align/Align.h	Thu May 21 16:21:57 2020 +0100
@@ -33,6 +33,26 @@
 public:
     Align() { }
 
+    enum AlignmentType {
+        NoAlignment,
+        LinearAlignment,
+        TrimmedLinearAlignment,
+        MATCHAlignment,
+        MATCHAlignmentWithPitchCompare,
+        SungPitchContourAlignment,
+        TransformDrivenDTWAlignment,
+        ExternalProgramAlignment,
+
+        LastAlignmentType = ExternalProgramAlignment
+    };
+
+    static QString getAlignmentTypeTag(AlignmentType type);
+    static AlignmentType getAlignmentTypeForTag(QString tag);
+
+    static AlignmentType getAlignmentPreference(QString &additionalData);
+    static void setAlignmentPreference(AlignmentType type,
+                                       QString additionalData = "");
+    
     /**
      * Align the "other" model to the reference, attaching an
      * AlignmentModel to it. Alignment is carried out by the method
@@ -84,6 +104,8 @@
      */
     static bool canAlign();
 
+    //!!! + check whether specific alignment types are available
+
 signals:
     /**
      * Emitted when an alignment is successfully completed. The
@@ -111,10 +133,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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/LinearAligner.cpp	Thu May 21 16:21:57 2020 +0100
@@ -0,0 +1,96 @@
+/* -*- 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"
+
+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;
+            usleep(500000);
+        }
+    }
+
+    auto reference = ModelById::get(m_reference);
+    auto toAlign = ModelById::get(m_toAlign);
+
+    if (!reference || !reference->isOK() ||
+        !toAlign || !toAlign->isOK()) {
+        return;
+    }
+    
+    sv_frame_t s0 = reference->getStartFrame(), s1 = toAlign->getStartFrame();
+    sv_frame_t e0 = reference->getEndFrame(), e1 = toAlign->getEndFrame();
+    sv_frame_t d0 = e0 - s0, d1 = e1 - s1;
+
+    if (d1 == 0) {
+        return;
+    }
+    
+    double ratio = double(d0) / double(d1);
+    sv_frame_t resolution = 1024;
+    
+    Path path(reference->getSampleRate(), resolution);
+
+    for (sv_frame_t f = s1; f < e1; f += resolution) {
+        sv_frame_t target = 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);
+    toAlign->setAlignment(alignmentModelId);
+    m_document->addNonDerivedModel(alignmentModelId);
+}
+
+//!!! + trimmed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/LinearAligner.h	Thu May 21 16:21:57 2020 +0100
@@ -0,0 +1,48 @@
+/* -*- 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;
+};
+
+#endif
--- a/align/TransformAligner.cpp	Wed May 06 11:45:27 2020 +0100
+++ b/align/TransformAligner.cpp	Thu May 21 16:21:57 2020 +0100
@@ -29,10 +29,12 @@
 
 TransformAligner::TransformAligner(Document *doc,
                                    ModelId reference,
-                                   ModelId toAlign) :
+                                   ModelId toAlign,
+                                   bool withTuningDifference) :
     m_document(doc),
     m_reference(reference),
     m_toAlign(toAlign),
+    m_withTuningDifference(withTuningDifference),
     m_tuningFrequency(440.f),
     m_incomplete(true)
 {
@@ -58,9 +60,9 @@
 {
     QSettings settings;
     settings.beginGroup("Alignment");
-    TransformId id =
-        settings.value("transform-id",
-                       "vamp:match-vamp-plugin:match:path").toString();
+    TransformId id = settings.value
+        ("transform-id",
+         "vamp:match-vamp-plugin:match:path").toString();
     settings.endGroup();
     return id;
 }
@@ -70,15 +72,10 @@
 {
     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();
-    }
+    TransformId id = settings.value
+        ("tuning-difference-transform-id",
+         "vamp:tuning-difference:tuning-difference:tuningfreq")
+        .toString();
     settings.endGroup();
     return id;
 }
@@ -157,7 +154,10 @@
         (m_reference, m_toAlign, ModelId());
     m_alignmentModel = ModelById::add(alignmentModel);
 
-    TransformId tdId = getTuningDifferenceTransformName();
+    TransformId tdId;
+    if (m_withTuningDifference) {
+        tdId = getTuningDifferenceTransformName();
+    }
 
     if (tdId == "") {
         
--- a/align/TransformAligner.h	Wed May 06 11:45:27 2020 +0100
+++ b/align/TransformAligner.h	Thu May 21 16:21:57 2020 +0100
@@ -27,7 +27,10 @@
 public:
     TransformAligner(Document *doc,
                      ModelId reference,
-                     ModelId toAlign);
+                     ModelId toAlign,
+                     bool withTuningDifference);
+
+    //!!! pass in transform id
 
     // Destroy the aligner, cleanly cancelling any ongoing alignment
     ~TransformAligner();
@@ -54,6 +57,7 @@
     ModelId m_tuningDiffProgressModel; // SparseTimeValueModel, unreg'd with doc
     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;
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/TransformDTWAligner.cpp	Thu May 21 16:21:57 2020 +0100
@@ -0,0 +1,390 @@
+/* -*- 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/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;
+
+TransformDTWAligner::TransformDTWAligner(Document *doc,
+                                         ModelId reference,
+                                         ModelId toAlign,
+                                         Transform transform,
+                                         DTWType dtwType) :
+    m_document(doc),
+    m_reference(reference),
+    m_toAlign(toAlign),
+    m_referenceTransformComplete(false),
+    m_toAlignTransformComplete(false),
+    m_transform(transform),
+    m_dtwType(dtwType),
+    m_incomplete(true)
+{
+}
+
+TransformDTWAligner::~TransformDTWAligner()
+{
+    if (m_incomplete) {
+        if (auto toAlign = ModelById::get(m_toAlign)) {
+            toAlign->setAlignment({});
+        }
+    }
+    
+    ModelById::release(m_referenceOutputModel);
+    ModelById::release(m_toAlignOutputModel);
+    ModelById::release(m_alignmentProgressModel);
+}
+
+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;
+    }
+
+    SVCERR << "TransformDTWAligner[" << this << "]: begin(): transform id "
+           << m_transform.getIdentifier()
+           << " is running on reference model" << endl;
+
+    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;
+    }
+
+    SVCERR << "TransformDTWAligner[" << this << "]: begin(): transform id "
+           << m_transform.getIdentifier()
+           << " is running on toAlign model" << endl;
+
+    connect(referenceOutputModel.get(), SIGNAL(completionChanged(ModelId)),
+            this, SLOT(completionChanged(ModelId)));
+    connect(toAlignOutputModel.get(), SIGNAL(completionChanged(ModelId)),
+            this, SLOT(completionChanged(ModelId)));
+
+    auto alignmentProgressModel = std::make_shared<SparseTimeValueModel>
+        (reference->getSampleRate(), m_transform.getStepSize(), false);
+    alignmentProgressModel->setCompletion(0);
+    m_alignmentProgressModel = ModelById::add(alignmentProgressModel);
+    
+    auto alignmentModel = std::make_shared<AlignmentModel>
+        (m_reference, m_toAlign, m_alignmentProgressModel);
+    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;
+    }
+
+    SVCERR << "TransformDTWAligner[" << this << "]: completionChanged: "
+           << "model " << id << endl;
+
+    auto referenceOutputModel = ModelById::get(m_referenceOutputModel);
+    auto toAlignOutputModel = ModelById::get(m_toAlignOutputModel);
+
+    if (!referenceOutputModel || !toAlignOutputModel) {
+        return;
+    }
+
+    int referenceCompletion = 0, toAlignCompletion = 0;
+    bool referenceReady = referenceOutputModel->isReady(&referenceCompletion);
+    bool toAlignReady = toAlignOutputModel->isReady(&toAlignCompletion);
+
+    auto alignmentProgressModel =
+        ModelById::getAs<SparseTimeValueModel>(m_alignmentProgressModel);
+    
+    if (referenceReady && toAlignReady) {
+
+        SVCERR << "TransformDTWAligner[" << this << "]: completionChanged: "
+               << "ready, calling performAlignment" << endl;
+
+        if (alignmentProgressModel) {
+            alignmentProgressModel->setCompletion(95);
+        }
+        
+        if (performAlignment()) {
+            emit complete(m_alignmentModel);
+        } else {
+            emit failed(m_toAlign, tr("Alignment of transform outputs failed"));
+        }
+
+    } else {
+
+        SVCERR << "TransformDTWAligner[" << this << "]: completionChanged: "
+               << "not ready yet: reference completion " << referenceCompletion
+               << ", toAlign completion " << toAlignCompletion << endl;
+
+        if (alignmentProgressModel) {
+            int completion = std::min(referenceCompletion,
+                                      toAlignCompletion);
+            completion = (completion * 94) / 100;
+            alignmentProgressModel->setCompletion(completion);
+        }
+    }
+}
+
+bool
+TransformDTWAligner::performAlignment()
+{
+    if (m_dtwType == Magnitude) {
+        return performAlignmentMagnitude();
+    } else {
+        return performAlignmentRiseFall();
+    }
+}
+
+bool
+TransformDTWAligner::performAlignmentMagnitude()
+{
+    auto referenceOutputSTVM = ModelById::getAs<SparseTimeValueModel>
+        (m_referenceOutputModel);
+    auto toAlignOutputSTVM = ModelById::getAs<SparseTimeValueModel>
+        (m_toAlignOutputModel);
+    auto alignmentModel = ModelById::getAs<AlignmentModel>
+        (m_alignmentModel);
+
+    if (!referenceOutputSTVM || !toAlignOutputSTVM) {
+        //!!! what?
+        return false;
+    }
+
+    if (!alignmentModel) {
+        return false;
+    }
+    
+    vector<double> s1, s2;
+
+    {
+        auto events = referenceOutputSTVM->getAllEvents();
+        for (auto e: events) {
+            s1.push_back(e.getValue());
+        }
+        events = toAlignOutputSTVM->getAllEvents();
+        for (auto e: events) {
+            s2.push_back(e.getValue());
+        }
+    }
+
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignment: "
+           << "Have " << s1.size() << " events from reference, "
+           << s2.size() << " from toAlign" << endl;
+
+    MagnitudeDTW dtw;
+    vector<size_t> alignment;
+
+    {
+        SVCERR << "TransformDTWAligner[" << this
+               << "]: serialising DTW to avoid over-allocation" << endl;
+        static QMutex mutex;
+        QMutexLocker locker(&mutex);
+
+        alignment = dtw.alignSeries(s1, s2);
+    }
+
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignment: "
+           << "DTW produced " << alignment.size() << " points:" << endl;
+    for (int i = 0; i < alignment.size() && i < 100; ++i) {
+        SVCERR << alignment[i] << " ";
+    }
+    SVCERR << endl;
+
+    auto alignmentProgressModel =
+        ModelById::getAs<SparseTimeValueModel>(m_alignmentProgressModel);
+    if (alignmentProgressModel) {
+        alignmentProgressModel->setCompletion(100);
+    }
+    
+    // clear the alignment progress model
+    alignmentModel->setPathFrom(ModelId());
+
+    sv_frame_t resolution = referenceOutputSTVM->getResolution();
+    sv_frame_t sourceFrame = 0;
+    
+    Path path(referenceOutputSTVM->getSampleRate(), resolution);
+    
+    for (size_t m: alignment) {
+        path.add(PathPoint(sourceFrame, sv_frame_t(m) * resolution));
+        sourceFrame += resolution;
+    }
+
+    alignmentModel->setPath(path);
+
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignment: Done"
+           << endl;
+
+    m_incomplete = false;
+    return true;
+}
+
+bool
+TransformDTWAligner::performAlignmentRiseFall()
+{
+    auto referenceOutputSTVM = ModelById::getAs<SparseTimeValueModel>
+        (m_referenceOutputModel);
+    auto toAlignOutputSTVM = ModelById::getAs<SparseTimeValueModel>
+        (m_toAlignOutputModel);
+    auto alignmentModel = ModelById::getAs<AlignmentModel>
+        (m_alignmentModel);
+
+    if (!referenceOutputSTVM || !toAlignOutputSTVM) {
+        //!!! what?
+        return false;
+    }
+
+    if (!alignmentModel) {
+        return false;
+    }
+    
+    vector<RiseFallDTW::Value> s1, s2;
+    double prev1 = 0.0, prev2 = 0.0;
+
+    {
+        auto events = referenceOutputSTVM->getAllEvents();
+        for (auto e: events) {
+            double v = e.getValue();
+            //!!! the original does this using MIDI pitch for the
+            //!!! pYin transform... rework with a lambda passed in
+            //!!! for modification maybe? + factor out s1/s2 of course
+            if (v > prev1) {
+                s1.push_back({ RiseFallDTW::Direction::Up, v - prev1 });
+            } else {
+                s1.push_back({ RiseFallDTW::Direction::Down, prev1 - v });
+            }
+            prev1 = v;
+        }
+        events = toAlignOutputSTVM->getAllEvents();
+        for (auto e: events) {
+            double v = e.getValue();
+            //!!! as above
+            if (v > prev2) {
+                s2.push_back({ RiseFallDTW::Direction::Up, v - prev2 });
+            } else {
+                s2.push_back({ RiseFallDTW::Direction::Down, prev2 - v });
+            }
+            prev2 = v;
+        }
+    }
+
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignment: "
+           << "Have " << s1.size() << " events from reference, "
+           << s2.size() << " from toAlign" << endl;
+
+    RiseFallDTW dtw;
+    
+    vector<size_t> alignment;
+
+    {
+        SVCERR << "TransformDTWAligner[" << this
+               << "]: serialising DTW to avoid over-allocation" << endl;
+        static QMutex mutex;
+        QMutexLocker locker(&mutex);
+
+        alignment = dtw.alignSeries(s1, s2);
+    }
+
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignment: "
+           << "DTW produced " << alignment.size() << " points:" << endl;
+    for (int i = 0; i < alignment.size() && i < 100; ++i) {
+        SVCERR << alignment[i] << " ";
+    }
+    SVCERR << endl;
+
+    auto alignmentProgressModel =
+        ModelById::getAs<SparseTimeValueModel>(m_alignmentProgressModel);
+    if (alignmentProgressModel) {
+        alignmentProgressModel->setCompletion(100);
+    }
+    
+    // clear the alignment progress model
+    alignmentModel->setPathFrom(ModelId());
+
+    sv_frame_t resolution = referenceOutputSTVM->getResolution();
+    sv_frame_t sourceFrame = 0;
+    
+    Path path(referenceOutputSTVM->getSampleRate(), resolution);
+    
+    for (size_t m: alignment) {
+        path.add(PathPoint(sourceFrame, sv_frame_t(m) * resolution));
+        sourceFrame += resolution;
+    }
+
+    alignmentModel->setPath(path);
+
+    SVCERR << "TransformDTWAligner[" << this << "]: performAlignment: Done"
+           << endl;
+
+    m_incomplete = false;
+    return true;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/TransformDTWAligner.h	Thu May 21 16:21:57 2020 +0100
@@ -0,0 +1,70 @@
+/* -*- 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 "transform/Transform.h"
+
+class AlignmentModel;
+class Document;
+
+class TransformDTWAligner : public Aligner
+{
+    Q_OBJECT
+
+public:
+    enum DTWType {
+        Magnitude,
+        RiseFall
+    };
+    
+    TransformDTWAligner(Document *doc,
+                        ModelId reference,
+                        ModelId toAlign,
+                        Transform transform,
+                        DTWType dtwType);
+
+    // 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();
+    
+    Document *m_document;
+    ModelId m_reference;
+    ModelId m_toAlign;
+    ModelId m_referenceOutputModel;
+    ModelId m_toAlignOutputModel;
+    ModelId m_alignmentProgressModel;
+    ModelId m_alignmentModel;
+    bool m_referenceTransformComplete;
+    bool m_toAlignTransformComplete;
+    Transform m_transform;
+    DTWType m_dtwType;
+    bool m_incomplete;
+};
+
+#endif
--- a/files.pri	Wed May 06 11:45:27 2020 +0100
+++ b/files.pri	Thu May 21 16:21:57 2020 +0100
@@ -3,7 +3,9 @@
            align/Align.h \
            align/Aligner.h \
            align/ExternalProgramAligner.h \
+           align/LinearAligner.h \
            align/TransformAligner.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/LinearAligner.cpp \
            align/TransformAligner.cpp \
+           align/TransformDTWAligner.cpp \
            audio/AudioCallbackPlaySource.cpp \
            audio/AudioCallbackRecordTarget.cpp \
            audio/AudioGenerator.cpp \