Mercurial > hg > svapp
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, + ¢sOffset); + 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, - ¢sOffset); - 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; }