changeset 1869:cb9209ef373a startup-timing

Merge from default branch
author Chris Cannam
date Tue, 16 Jun 2020 17:44:06 +0100
parents 7b6e18380e8f (current diff) 44dba7cd9ec3 (diff)
children 258e356b1a7b
files
diffstat 17 files changed, 376 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- a/base/Event.h	Wed Jun 03 13:57:50 2020 +0100
+++ b/base/Event.h	Tue Jun 16 17:44:06 2020 +0100
@@ -84,7 +84,10 @@
         m_haveDuration(true), m_haveReferenceFrame(false),
         m_value(value), m_level(0.f), m_frame(frame),
         m_duration(duration), m_referenceFrame(0), m_label(label) {
-        if (m_duration < 0) throw std::logic_error("duration must be >= 0");
+        if (m_duration < 0) {
+            m_frame += m_duration;
+            m_duration = -m_duration;
+        }
     }
         
     Event(sv_frame_t frame, float value, sv_frame_t duration,
@@ -93,7 +96,10 @@
         m_haveDuration(true), m_haveReferenceFrame(false),
         m_value(value), m_level(level), m_frame(frame),
         m_duration(duration), m_referenceFrame(0), m_label(label) {
-        if (m_duration < 0) throw std::logic_error("duration must be >= 0");
+        if (m_duration < 0) {
+            m_frame += m_duration;
+            m_duration = -m_duration;
+        }
     }
 
     Event(const Event &event) =default;
@@ -135,7 +141,10 @@
         Event p(*this);
         p.m_duration = duration;
         p.m_haveDuration = true;
-        if (duration < 0) throw std::logic_error("duration must be >= 0");
+        if (p.m_duration < 0) {
+            p.m_frame += p.m_duration;
+            p.m_duration = -p.m_duration;
+        }
         return p;
     }
     Event withoutDuration() const {
--- a/base/Preferences.cpp	Wed Jun 03 13:57:50 2020 +0100
+++ b/base/Preferences.cpp	Tue Jun 16 17:44:06 2020 +0100
@@ -44,6 +44,7 @@
     m_omitRecentTemps(true),
     m_tempDirRoot(""),
     m_fixedSampleRate(0),
+    m_recordMono(false),
     m_resampleOnLoad(false),
     m_gapless(true),
     m_normaliseAudio(false),
@@ -67,6 +68,7 @@
         (settings.value("window-type", int(HanningWindow)).toInt());
     m_runPluginsInProcess = settings.value("run-vamp-plugins-in-process", true).toBool();
     m_fixedSampleRate = settings.value("fixed-sample-rate", 0).toDouble();
+    m_recordMono = settings.value("record-mono", false).toBool();
     m_resampleOnLoad = settings.value("resample-on-load", false).toBool();
     m_gapless = settings.value("gapless", true).toBool();
     m_normaliseAudio = settings.value("normalise-audio", false).toBool();
@@ -100,6 +102,7 @@
     props.push_back("Window Type");
     props.push_back("Resample Quality");
     props.push_back("Omit Temporaries from Recent Files");
+    props.push_back("Record Mono");
     props.push_back("Resample On Load");
     props.push_back("Use Gapless Mode");
     props.push_back("Normalise Audio");
@@ -141,6 +144,9 @@
     if (name == "Omit Temporaries from Recent Files") {
         return tr("Omit temporaries from Recent Files menu");
     }
+    if (name == "Record Mono") {
+        return tr("Mix recorded channels to mono");
+    }
     if (name == "Resample On Load") {
         return tr("Resample mismatching files on import");
     }
@@ -201,6 +207,9 @@
     if (name == "Omit Temporaries from Recent Files") {
         return ToggleProperty;
     }
+    if (name == "Record Mono") {
+        return ToggleProperty;
+    }
     if (name == "Resample On Load") {
         return ToggleProperty;
     }
@@ -552,6 +561,19 @@
 }
 
 void
+Preferences::setRecordMono(bool mono)
+{
+    if (m_recordMono != mono) {
+        m_recordMono = mono;
+        QSettings settings;
+        settings.beginGroup("Preferences");
+        settings.setValue("record-mono", mono);
+        settings.endGroup();
+        emit propertyChanged("Record Mono");
+    }
+}
+
+void
 Preferences::setResampleOnLoad(bool resample)
 {
     if (m_resampleOnLoad != resample) {
--- a/base/Preferences.h	Wed Jun 03 13:57:50 2020 +0100
+++ b/base/Preferences.h	Tue Jun 16 17:44:06 2020 +0100
@@ -65,6 +65,10 @@
 
     QString getTemporaryDirectoryRoot() const { return m_tempDirRoot; }
 
+    /// True if we should always mix down recorded audio to a single
+    /// channel regardless of how many channels the device opens
+    bool getRecordMono() const { return m_recordMono; }
+    
     /// If we should always resample audio to the same rate, return it; otherwise (the normal case) return 0
     sv_samplerate_t getFixedSampleRate() const { return m_fixedSampleRate; }
 
@@ -119,6 +123,7 @@
     void setOmitTempsFromRecentFiles(bool omit);
     void setTemporaryDirectoryRoot(QString tempDirRoot);
     void setFixedSampleRate(sv_samplerate_t);
+    void setRecordMono(bool);
     void setResampleOnLoad(bool);
     void setUseGaplessMode(bool);
     void setNormaliseAudio(bool);
@@ -157,6 +162,7 @@
     bool m_omitRecentTemps;
     QString m_tempDirRoot;
     sv_samplerate_t m_fixedSampleRate;
+    bool m_recordMono;
     bool m_resampleOnLoad;
     bool m_gapless;
     bool m_normaliseAudio;
--- a/data/fileio/CSVFileReader.cpp	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/fileio/CSVFileReader.cpp	Tue Jun 16 17:44:06 2020 +0100
@@ -117,17 +117,18 @@
     return m_error;
 }
 
-sv_frame_t
+bool
 CSVFileReader::convertTimeValue(QString s, int lineno,
                                 sv_samplerate_t sampleRate,
-                                int windowSize) const
+                                int windowSize,
+                                sv_frame_t &calculatedFrame) const
 {
     QRegExp nonNumericRx("[^0-9eE.,+-]");
     int warnLimit = 10;
 
     CSVFormat::TimeUnits timeUnits = m_format.getTimeUnits();
 
-    sv_frame_t calculatedFrame = 0;
+    calculatedFrame = 0;
 
     bool ok = false;
     QString numeric = s;
@@ -316,31 +317,37 @@
                 switch (modelType) {
 
                 case CSVFormat::OneDimensionalModel:
+                    SVDEBUG << "CSVFileReader: Creating sparse one-dimensional model" << endl;
                     model1 = new SparseOneDimensionalModel(sampleRate, windowSize);
                     model = model1;
                     break;
                 
                 case CSVFormat::TwoDimensionalModel:
+                    SVDEBUG << "CSVFileReader: Creating sparse time-value model" << endl;
                     model2 = new SparseTimeValueModel(sampleRate, windowSize, false);
                     model = model2;
                     break;
                 
                 case CSVFormat::TwoDimensionalModelWithDuration:
+                    SVDEBUG << "CSVFileReader: Creating region model" << endl;
                     model2a = new RegionModel(sampleRate, windowSize, false);
                     model = model2a;
                     break;
                 
                 case CSVFormat::TwoDimensionalModelWithDurationAndPitch:
+                    SVDEBUG << "CSVFileReader: Creating note model" << endl;
                     model2b = new NoteModel(sampleRate, windowSize, false);
                     model = model2b;
                     break;
                 
                 case CSVFormat::TwoDimensionalModelWithDurationAndExtent:
+                    SVDEBUG << "CSVFileReader: Creating box model" << endl;
                     model2c = new BoxModel(sampleRate, windowSize, false);
                     model = model2c;
                     break;
                 
                 case CSVFormat::ThreeDimensionalModel:
+                    SVDEBUG << "CSVFileReader: Creating editable dense three-dimensional model" << endl;
                     model3 = new EditableDenseThreeDimensionalModel
                         (sampleRate, windowSize, valueColumns);
                     model = model3;
@@ -348,6 +355,7 @@
 
                 case CSVFormat::WaveFileModel:
                 {
+                    SVDEBUG << "CSVFileReader: Creating writable wave-file model" << endl;
                     bool normalise = (m_format.getAudioSampleRange()
                                       == CSVFormat::SampleRangeOther);
                     QString path = getConvertedAudioFilePath();
@@ -387,6 +395,7 @@
             float otherValue = 0.f;
             float pitch = 0.f;
             QString label = "";
+            bool ok = true;
 
             duration = 0.f;
             haveEndTime = false;
@@ -403,16 +412,21 @@
                     break;
 
                 case CSVFormat::ColumnStartTime:
-                    frameNo = convertTimeValue(s, lineno, sampleRate, windowSize);
+                    if (!convertTimeValue(s, lineno, sampleRate, windowSize, frameNo)) {
+                        ok = false;
+                    }
                     break;
                 
                 case CSVFormat::ColumnEndTime:
-                    endFrame = convertTimeValue(s, lineno, sampleRate, windowSize);
-                    haveEndTime = true;
+                    if (convertTimeValue(s, lineno, sampleRate, windowSize, endFrame)) {
+                        haveEndTime = true;
+                    }
                     break;
 
                 case CSVFormat::ColumnDuration:
-                    duration = convertTimeValue(s, lineno, sampleRate, windowSize);
+                    if (!convertTimeValue(s, lineno, sampleRate, windowSize, duration)) {
+                        ok = false;
+                    }
                     break;
 
                 case CSVFormat::ColumnValue:
@@ -436,6 +450,10 @@
                 }
             }
 
+            if (!ok) {
+                continue;
+            }
+            
             ++labelCountMap[label];
             
             if (haveEndTime) { // ... calculate duration now all cols read
--- a/data/fileio/CSVFileReader.h	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/fileio/CSVFileReader.h	Tue Jun 16 17:44:06 2020 +0100
@@ -70,8 +70,8 @@
     mutable int m_progress;
     ProgressReporter *m_reporter;
 
-    sv_frame_t convertTimeValue(QString, int lineno, sv_samplerate_t sampleRate,
-                                int windowSize) const;
+    bool convertTimeValue(QString, int lineno, sv_samplerate_t sampleRate,
+                          int windowSize, sv_frame_t &calculatedFrame) const;
 
     QString getConvertedAudioFilePath() const;
 };
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/CSVReaderTest.h	Tue Jun 16 17:44:06 2020 +0100
@@ -0,0 +1,234 @@
+/* -*- 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 TEST_CSV_READER_H
+#define TEST_CSV_READER_H
+
+#include "../CSVFileReader.h"
+
+#include "data/model/SparseOneDimensionalModel.h"
+#include "data/model/SparseTimeValueModel.h"
+#include "data/model/RegionModel.h"
+#include "data/model/EditableDenseThreeDimensionalModel.h"
+
+#include "base/Debug.h"
+
+#include <cmath>
+
+#include <QObject>
+#include <QtTest>
+#include <QDir>
+
+#include <iostream>
+
+using namespace std;
+
+class CSVReaderTest : public QObject
+{
+    Q_OBJECT
+
+private:
+    QDir csvDir;
+    sv_samplerate_t mainRate;
+
+public:
+    CSVReaderTest(QString base) {
+        if (base == "") {
+            base = "svcore/data/fileio/test";
+        }
+        csvDir = QDir(base + "/csv");
+        mainRate = 44100;
+    }
+
+private:
+    void loadFrom(QString filename, Model *&model) {
+        QString path(csvDir.filePath(filename));
+        CSVFormat f;
+        f.guessFormatFor(path);
+        CSVFileReader reader(path, f, mainRate);
+        model = reader.load();
+        QVERIFY(model);
+        QVERIFY(reader.isOK());
+        QCOMPARE(reader.getError(), QString());
+    }
+
+private slots:
+    void init() {
+        if (!csvDir.exists()) {
+            SVCERR << "ERROR: CSV test file directory \"" << csvDir.absolutePath() << "\" does not exist" << endl;
+            QVERIFY2(csvDir.exists(), "CSV test file directory not found");
+        }
+    }
+
+    void modelType1DSamples() {
+        Model *model = nullptr;
+        loadFrom("model-type-1d-samples.csv", model);
+        auto actual = qobject_cast<SparseOneDimensionalModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        //!!! + the actual contents
+        delete model;
+    }
+
+    void modelType1DSeconds() {
+        Model *model = nullptr;
+        loadFrom("model-type-1d-seconds.csv", model);
+        auto actual = qobject_cast<SparseOneDimensionalModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+
+    void modelType2DDurationSamples() {
+        Model *model = nullptr;
+        loadFrom("model-type-2d-duration-samples.csv", model);
+        auto actual = qobject_cast<RegionModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void modelType2DDurationSeconds() {
+        Model *model = nullptr;
+        loadFrom("model-type-2d-duration-seconds.csv", model);
+        auto actual = qobject_cast<RegionModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void badNegativeDuration() {
+        Model *model = nullptr;
+        loadFrom("bad-negative-duration.csv", model);
+        auto actual = qobject_cast<RegionModel *>(model);
+        QVERIFY(actual);
+        //!!! + check duration has been corrected
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void modelType2DEndTimeSamples() {
+        Model *model = nullptr;
+        loadFrom("model-type-2d-endtime-samples.csv", model);
+        auto actual = qobject_cast<RegionModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void modelType2DEndTimeSeconds() {
+        Model *model = nullptr;
+        loadFrom("model-type-2d-endtime-seconds.csv", model);
+        auto actual = qobject_cast<RegionModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void modelType2DImplicit() {
+        Model *model = nullptr;
+        loadFrom("model-type-2d-implicit.csv", model);
+        auto actual = qobject_cast<SparseTimeValueModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void modelType2DSamples() {
+        Model *model = nullptr;
+        loadFrom("model-type-2d-samples.csv", model);
+        auto actual = qobject_cast<SparseTimeValueModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void modelType2DSeconds() {
+        Model *model = nullptr;
+        loadFrom("model-type-2d-seconds.csv", model);
+        auto actual = qobject_cast<SparseTimeValueModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void modelType3DImplicit() {
+        Model *model = nullptr;
+        loadFrom("model-type-3d-implicit.csv", model);
+        auto actual = qobject_cast<EditableDenseThreeDimensionalModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getWidth(), 6);
+        QCOMPARE(actual->getHeight(), 6);
+        delete model;
+    }
+    
+    void modelType3DSamples() {
+        Model *model = nullptr;
+        loadFrom("model-type-3d-samples.csv", model);
+        auto actual = qobject_cast<EditableDenseThreeDimensionalModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getWidth(), 6);
+        QCOMPARE(actual->getHeight(), 6);
+        delete model;
+    }
+    
+    void modelType3DSeconds() {
+        Model *model = nullptr;
+        loadFrom("model-type-3d-seconds.csv", model);
+        auto actual = qobject_cast<EditableDenseThreeDimensionalModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getWidth(), 6);
+        QCOMPARE(actual->getHeight(), 6);
+        delete model;
+    }
+    
+    void withBlankLines1D() {
+        Model *model = nullptr;
+        loadFrom("with-blank-lines-1d.csv", model);
+        auto actual = qobject_cast<SparseOneDimensionalModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void withBlankLines2D() {
+        Model *model = nullptr;
+        loadFrom("with-blank-lines-2d.csv", model);
+        auto actual = qobject_cast<SparseTimeValueModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+    
+    void withBlankLines3D() {
+        Model *model = nullptr;
+        loadFrom("with-blank-lines-3d.csv", model);
+        auto actual = qobject_cast<EditableDenseThreeDimensionalModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getWidth(), 6);
+        QCOMPARE(actual->getHeight(), 6);
+        delete model;
+    }
+
+    void quoting() {
+        Model *model = nullptr;
+        loadFrom("quoting.csv", model);
+        auto actual = qobject_cast<SparseTimeValueModel *>(model);
+        QVERIFY(actual);
+        QCOMPARE(actual->getAllEvents().size(), 5);
+        delete model;
+    }
+};
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/csv/bad-negative-duration.csv	Tue Jun 16 17:44:06 2020 +0100
@@ -0,0 +1,6 @@
+# As model-type-2d-duration-seconds.csv but with a negative value for a duration
+1.1,4,620
+2.2,4.2,880
+3.3,0.4,440
+4.4,3.8,213
+5.5,-2.3,123
--- a/data/fileio/test/csv/model-type-2d-duration-seconds.csv	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/fileio/test/csv/model-type-2d-duration-seconds.csv	Tue Jun 16 17:44:06 2020 +0100
@@ -5,4 +5,4 @@
 2.2,4.2,880
 3.3,0.4,440
 4.4,3.8,213
-5.5,-2.3,123
+5.5,2.3,123
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/csv/quoting.csv	Tue Jun 16 17:44:06 2020 +0100
@@ -0,0 +1,5 @@
+1,2,Label 1
+"2",4
+3,"6","Labels 3a, 3b"
+4,8.0,"Label \"4\""
+5, 1,"Label ""5"""
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/csv/with-blank-lines-1d.csv	Tue Jun 16 17:44:06 2020 +0100
@@ -0,0 +1,9 @@
+
+3.2	First thing
+ 
+4.4	Second thing
+5.5	Third thing
+
+6.3	Fourth thing
+7.8	Fifth thing
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/csv/with-blank-lines-2d.csv	Tue Jun 16 17:44:06 2020 +0100
@@ -0,0 +1,12 @@
+
+45678,4
+
+123239,4.2
+
320130,0.4
+
+# Also include some CR/LF variations:
+452103,3.8
+
+
+# And let's not have a newline after this last line:
+620301,-2.3
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/data/fileio/test/csv/with-blank-lines-3d.csv	Tue Jun 16 17:44:06 2020 +0100
@@ -0,0 +1,10 @@
+
+22050,143.0,2.0,-1.3,0.0,0.0,1.0
+44100,0.2,0.1,-3.0,0.0,0.1,0.143
+
+66150,0.143,0.2,-3.1,0.0,0.0,0.1
+88200,2.0,1.0,-0.3,0.0,1.0,143.0
+ 
+110250,0.0,0.0,0.1,0.143,0.2,-3.1
+132300,0.0,1.0,143.0,2.0,1.0,-0.3
+
--- a/data/fileio/test/files.pri	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/fileio/test/files.pri	Tue Jun 16 17:44:06 2020 +0100
@@ -9,6 +9,7 @@
 	EncodingTest.h \
 	MIDIFileReaderTest.h \
 	CSVFormatTest.h \
+	CSVReaderTest.h \
 	CSVStreamWriterTest.h
      
 TEST_SOURCES += \
--- a/data/fileio/test/svcore-data-fileio-test.cpp	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/fileio/test/svcore-data-fileio-test.cpp	Tue Jun 16 17:44:06 2020 +0100
@@ -18,6 +18,7 @@
 #include "EncodingTest.h"
 #include "MIDIFileReaderTest.h"
 #include "CSVFormatTest.h"
+#include "CSVReaderTest.h"
 #include "CSVStreamWriterTest.h"
 
 #include "system/Init.h"
@@ -90,6 +91,12 @@
     }
 
     {
+        CSVReaderTest t(testDir);
+        if (QTest::qExec(&t, argc, argv) == 0) ++good;
+        else ++bad;
+    }
+
+    {
         CSVStreamWriterTest t;
         if (QTest::qExec(&t, argc, argv) == 0) ++good;
         else ++bad;
--- a/data/model/AlignmentModel.cpp	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/model/AlignmentModel.cpp	Tue Jun 16 17:44:06 2020 +0100
@@ -29,7 +29,8 @@
     m_reversePath(nullptr),
     m_pathBegun(false),
     m_pathComplete(false),
-    m_relativePitch(0)
+    m_relativePitch(0),
+    m_explicitlySetCompletion(-1)
 {
     setPathFrom(pathSource);
 
@@ -105,6 +106,10 @@
 bool
 AlignmentModel::isReady(int *completion) const
 {
+    if (m_explicitlySetCompletion != -1) {
+        if (completion) *completion = m_explicitlySetCompletion;
+        return (m_explicitlySetCompletion == 100);
+    }
     if (!m_pathBegun && !m_pathSource.isNone()) {
         if (completion) *completion = 0;
 #ifdef DEBUG_ALIGNMENT_MODEL
@@ -156,6 +161,13 @@
     return m_aligned;
 }
 
+void
+AlignmentModel::setCompletion(int completion)
+{
+    m_explicitlySetCompletion = completion;
+    emit completionChanged(getId());
+}
+
 sv_frame_t
 AlignmentModel::toReference(sv_frame_t frame) const
 {
@@ -310,9 +322,9 @@
 AlignmentModel::performAlignment(const Path &path, sv_frame_t frame) const
 {
     // The path consists of a series of points, each with frame equal
-    // to the frame on the source model and mapframe equal to the
-    // frame on the target model.  Both should be monotonically
-    // increasing.
+    // to the frame on the source model (aligned model) and mapframe
+    // equal to the frame on the target model (reference model).  Both
+    // should be monotonically increasing.
 
     const Path::Points &points = path.getPoints();
 
--- a/data/model/AlignmentModel.h	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/model/AlignmentModel.h	Tue Jun 16 17:44:06 2020 +0100
@@ -56,6 +56,8 @@
     ModelId getReferenceModel() const;
     ModelId getAlignedModel() const;
 
+    void setCompletion(int completion);
+
     sv_frame_t toReference(sv_frame_t frame) const;
     sv_frame_t fromReference(sv_frame_t frame) const;
 
@@ -107,6 +109,7 @@
     bool m_pathComplete;
     QString m_error;
     int m_relativePitch;
+    int m_explicitlySetCompletion;
 
     void constructPath() const;
     void constructReversePath() const;
--- a/data/model/Path.h	Wed Jun 03 13:57:50 2020 +0100
+++ b/data/model/Path.h	Tue Jun 16 17:44:06 2020 +0100
@@ -29,6 +29,11 @@
     PathPoint(sv_frame_t _frame, sv_frame_t _mapframe) :
         frame(_frame), mapframe(_mapframe) { }
 
+    // "The path consists of a series of points, each with frame equal
+    // to the frame on the source model (aligned model) and mapframe
+    // equal to the frame on the target model (reference model).  Both
+    // should be monotonically increasing."
+
     sv_frame_t frame;
     sv_frame_t mapframe;