annotate align/TransformAligner.cpp @ 776:32e66fcc4cb7 pitch-align

Make querying and setting the external alignment program or transform separate from selecting the alignment type - we need it to work that way for a clearer UI
author Chris Cannam
date Thu, 25 Jun 2020 09:32:01 +0100
parents a316cb6fed81
children
rev   line source
Chris@752 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@752 2
Chris@752 3 /*
Chris@752 4 Sonic Visualiser
Chris@752 5 An audio file viewer and annotation editor.
Chris@752 6 Centre for Digital Music, Queen Mary, University of London.
Chris@752 7
Chris@752 8 This program is free software; you can redistribute it and/or
Chris@752 9 modify it under the terms of the GNU General Public License as
Chris@752 10 published by the Free Software Foundation; either version 2 of the
Chris@752 11 License, or (at your option) any later version. See the file
Chris@752 12 COPYING included with this distribution for more information.
Chris@752 13 */
Chris@752 14
Chris@752 15 #include "TransformAligner.h"
Chris@752 16
Chris@752 17 #include "data/model/SparseTimeValueModel.h"
Chris@752 18 #include "data/model/RangeSummarisableTimeValueModel.h"
Chris@752 19 #include "data/model/AlignmentModel.h"
Chris@752 20 #include "data/model/AggregateWaveModel.h"
Chris@752 21
Chris@752 22 #include "framework/Document.h"
Chris@752 23
Chris@752 24 #include "transform/TransformFactory.h"
Chris@752 25 #include "transform/ModelTransformerFactory.h"
Chris@752 26 #include "transform/FeatureExtractionModelTransformer.h"
Chris@752 27
Chris@752 28 #include <QSettings>
Chris@752 29
Chris@752 30 TransformAligner::TransformAligner(Document *doc,
Chris@752 31 ModelId reference,
Chris@767 32 ModelId toAlign,
Chris@767 33 bool withTuningDifference) :
Chris@752 34 m_document(doc),
Chris@752 35 m_reference(reference),
Chris@752 36 m_toAlign(toAlign),
Chris@767 37 m_withTuningDifference(withTuningDifference),
Chris@752 38 m_tuningFrequency(440.f),
Chris@752 39 m_incomplete(true)
Chris@752 40 {
Chris@752 41 }
Chris@752 42
Chris@752 43 TransformAligner::~TransformAligner()
Chris@752 44 {
Chris@752 45 if (m_incomplete) {
Chris@752 46 auto other =
Chris@752 47 ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
Chris@752 48 if (other) {
Chris@752 49 other->setAlignment({});
Chris@752 50 }
Chris@752 51 }
Chris@752 52
Chris@752 53 ModelById::release(m_tuningDiffOutputModel);
Chris@752 54 ModelById::release(m_pathOutputModel);
Chris@752 55 }
Chris@752 56
Chris@752 57 QString
Chris@752 58 TransformAligner::getAlignmentTransformName()
Chris@752 59 {
Chris@752 60 QSettings settings;
Chris@752 61 settings.beginGroup("Alignment");
Chris@767 62 TransformId id = settings.value
Chris@767 63 ("transform-id",
Chris@767 64 "vamp:match-vamp-plugin:match:path").toString();
Chris@752 65 settings.endGroup();
Chris@752 66 return id;
Chris@752 67 }
Chris@752 68
Chris@752 69 QString
Chris@752 70 TransformAligner::getTuningDifferenceTransformName()
Chris@752 71 {
Chris@752 72 QSettings settings;
Chris@752 73 settings.beginGroup("Alignment");
Chris@767 74 TransformId id = settings.value
Chris@767 75 ("tuning-difference-transform-id",
Chris@767 76 "vamp:tuning-difference:tuning-difference:tuningfreq")
Chris@767 77 .toString();
Chris@752 78 settings.endGroup();
Chris@752 79 return id;
Chris@752 80 }
Chris@752 81
Chris@752 82 bool
Chris@752 83 TransformAligner::isAvailable()
Chris@752 84 {
Chris@752 85 TransformFactory *factory = TransformFactory::getInstance();
Chris@752 86 TransformId id = getAlignmentTransformName();
Chris@752 87 TransformId tdId = getTuningDifferenceTransformName();
Chris@752 88 return factory->haveTransform(id) &&
Chris@752 89 (tdId == "" || factory->haveTransform(tdId));
Chris@752 90 }
Chris@752 91
Chris@761 92 void
Chris@761 93 TransformAligner::begin()
Chris@752 94 {
Chris@752 95 auto reference =
Chris@752 96 ModelById::getAs<RangeSummarisableTimeValueModel>(m_reference);
Chris@752 97 auto other =
Chris@752 98 ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
Chris@752 99
Chris@761 100 if (!reference || !other) return;
Chris@752 101
Chris@752 102 // This involves creating a number of new models:
Chris@752 103 //
Chris@752 104 // 1. an AggregateWaveModel to provide the mixdowns of the main
Chris@752 105 // model and the new model in its two channels, as input to the
Chris@752 106 // MATCH plugin. We just call this one aggregateModel
Chris@752 107 //
Chris@752 108 // 2a. a SparseTimeValueModel which will be automatically created
Chris@752 109 // by FeatureExtractionModelTransformer when running the
Chris@752 110 // TuningDifference plugin to receive the relative tuning of the
Chris@752 111 // second model (if pitch-aware alignment is enabled in the
Chris@752 112 // preferences). This is m_tuningDiffOutputModel.
Chris@752 113 //
Chris@752 114 // 2b. a SparseTimeValueModel which will be automatically created
Chris@752 115 // by FeatureExtractionPluginTransformer when running the MATCH
Chris@752 116 // plugin to perform alignment (so containing the alignment path).
Chris@752 117 // This is m_pathOutputModel.
Chris@752 118 //
Chris@752 119 // 3. an AlignmentModel, which stores the path and carries out
Chris@752 120 // alignment lookups on it. This one is m_alignmentModel.
Chris@752 121 //
Chris@752 122 // Models 1 and 3 are registered with the document, which will
Chris@752 123 // eventually release them. We don't release them here except in
Chris@752 124 // the case where an activity fails before the point where we
Chris@752 125 // would otherwise have registered them with the document.
Chris@752 126 //
Chris@768 127 // Models 2a (m_tuningDiffOutputModel) and 2b (m_pathOutputModel)
Chris@768 128 // are not registered with the document, because they are not
Chris@768 129 // intended to persist. These have to be released by us when
Chris@752 130 // finished with, but their lifespans do not extend beyond the end
Chris@752 131 // of the alignment procedure, so this should be ok.
Chris@752 132
Chris@752 133 AggregateWaveModel::ChannelSpecList components;
Chris@752 134 components.push_back
Chris@752 135 (AggregateWaveModel::ModelChannelSpec(m_reference, -1));
Chris@752 136
Chris@752 137 components.push_back
Chris@752 138 (AggregateWaveModel::ModelChannelSpec(m_toAlign, -1));
Chris@752 139
Chris@752 140 auto aggregateModel = std::make_shared<AggregateWaveModel>(components);
Chris@752 141 m_aggregateModel = ModelById::add(aggregateModel);
Chris@752 142 m_document->addNonDerivedModel(m_aggregateModel);
Chris@752 143
Chris@752 144 auto alignmentModel = std::make_shared<AlignmentModel>
Chris@752 145 (m_reference, m_toAlign, ModelId());
Chris@752 146 m_alignmentModel = ModelById::add(alignmentModel);
Chris@752 147
Chris@767 148 TransformId tdId;
Chris@767 149 if (m_withTuningDifference) {
Chris@767 150 tdId = getTuningDifferenceTransformName();
Chris@767 151 }
Chris@752 152
Chris@752 153 if (tdId == "") {
Chris@752 154
Chris@752 155 if (beginAlignmentPhase()) {
Chris@752 156 other->setAlignment(m_alignmentModel);
Chris@752 157 m_document->addNonDerivedModel(m_alignmentModel);
Chris@752 158 } else {
Chris@761 159 QString error = alignmentModel->getError();
Chris@752 160 ModelById::release(alignmentModel);
Chris@761 161 emit failed(m_toAlign, error);
Chris@761 162 return;
Chris@752 163 }
Chris@752 164
Chris@752 165 } else {
Chris@752 166
Chris@752 167 // Have a tuning-difference transform id, so run it
Chris@752 168 // asynchronously first
Chris@752 169
Chris@752 170 TransformFactory *tf = TransformFactory::getInstance();
Chris@752 171
Chris@752 172 Transform transform = tf->getDefaultTransformFor
Chris@752 173 (tdId, aggregateModel->getSampleRate());
Chris@752 174
Chris@752 175 transform.setParameter("maxduration", 60);
Chris@752 176 transform.setParameter("maxrange", 6);
Chris@752 177 transform.setParameter("finetuning", false);
Chris@752 178
Chris@752 179 SVDEBUG << "TransformAligner: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
Chris@752 180
Chris@752 181 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
Chris@752 182
Chris@752 183 QString message;
Chris@753 184 m_tuningDiffOutputModel = mtf->transform(transform,
Chris@753 185 m_aggregateModel,
Chris@753 186 message);
Chris@752 187
Chris@752 188 auto tuningDiffOutputModel =
Chris@753 189 ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
Chris@752 190 if (!tuningDiffOutputModel) {
Chris@752 191 SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
Chris@752 192 ModelById::release(alignmentModel);
Chris@761 193 emit failed(m_toAlign, message);
Chris@761 194 return;
Chris@752 195 }
Chris@752 196
Chris@752 197 other->setAlignment(m_alignmentModel);
Chris@752 198 m_document->addNonDerivedModel(m_alignmentModel);
Chris@752 199
Chris@752 200 connect(tuningDiffOutputModel.get(),
Chris@752 201 SIGNAL(completionChanged(ModelId)),
Chris@752 202 this, SLOT(tuningDifferenceCompletionChanged(ModelId)));
Chris@752 203 }
Chris@752 204 }
Chris@752 205
Chris@752 206 void
Chris@752 207 TransformAligner::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId)
Chris@752 208 {
Chris@761 209 if (m_tuningDiffOutputModel.isNone()) {
Chris@761 210 // we're done, this is probably a spurious queued event
Chris@761 211 return;
Chris@761 212 }
Chris@761 213
Chris@752 214 if (tuningDiffOutputModelId != m_tuningDiffOutputModel) {
Chris@752 215 SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged: Model "
Chris@753 216 << tuningDiffOutputModelId
Chris@753 217 << " is not ours! (ours is "
Chris@753 218 << m_tuningDiffOutputModel << ")" << endl;
Chris@752 219 return;
Chris@752 220 }
Chris@752 221
Chris@752 222 auto tuningDiffOutputModel =
Chris@752 223 ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
Chris@752 224 if (!tuningDiffOutputModel) {
Chris@752 225 SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged: Model "
Chris@752 226 << tuningDiffOutputModelId
Chris@752 227 << " not known as SparseTimeValueModel" << endl;
Chris@752 228 return;
Chris@752 229 }
Chris@752 230
Chris@752 231 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
Chris@752 232 if (!alignmentModel) {
Chris@752 233 SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged:"
Chris@752 234 << "alignment model has disappeared" << endl;
Chris@752 235 return;
Chris@752 236 }
Chris@752 237
Chris@752 238 int completion = 0;
Chris@752 239 bool done = tuningDiffOutputModel->isReady(&completion);
Chris@752 240
Chris@761 241 SVDEBUG << "TransformAligner::tuningDifferenceCompletionChanged: model "
Chris@761 242 << m_tuningDiffOutputModel << ", completion = " << completion
Chris@761 243 << ", done = " << done << endl;
Chris@761 244
Chris@752 245 if (!done) {
Chris@752 246 // This will be the completion the alignment model reports,
Chris@769 247 // before the alignment actually begins
Chris@769 248 alignmentModel->setCompletion(completion / 2);
Chris@752 249 return;
Chris@752 250 }
Chris@752 251
Chris@752 252 m_tuningFrequency = 440.f;
Chris@752 253
Chris@752 254 if (!tuningDiffOutputModel->isEmpty()) {
Chris@752 255 m_tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue();
Chris@752 256 SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: Reported tuning frequency = " << m_tuningFrequency << endl;
Chris@752 257 } else {
Chris@752 258 SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
Chris@752 259 }
Chris@752 260
Chris@752 261 ModelById::release(tuningDiffOutputModel);
Chris@752 262 m_tuningDiffOutputModel = {};
Chris@752 263
Chris@752 264 beginAlignmentPhase();
Chris@752 265 }
Chris@752 266
Chris@752 267 bool
Chris@752 268 TransformAligner::beginAlignmentPhase()
Chris@752 269 {
Chris@752 270 TransformId id = getAlignmentTransformName();
Chris@752 271
Chris@753 272 SVDEBUG << "TransformAligner::beginAlignmentPhase: transform is "
Chris@753 273 << id << endl;
Chris@753 274
Chris@752 275 TransformFactory *tf = TransformFactory::getInstance();
Chris@752 276
Chris@752 277 auto aggregateModel =
Chris@752 278 ModelById::getAs<AggregateWaveModel>(m_aggregateModel);
Chris@752 279 auto alignmentModel =
Chris@752 280 ModelById::getAs<AlignmentModel>(m_alignmentModel);
Chris@752 281
Chris@752 282 if (!aggregateModel || !alignmentModel) {
Chris@752 283 SVCERR << "TransformAligner::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl;
Chris@752 284 return false;
Chris@752 285 }
Chris@752 286
Chris@752 287 Transform transform = tf->getDefaultTransformFor
Chris@752 288 (id, aggregateModel->getSampleRate());
Chris@752 289
Chris@752 290 transform.setStepSize(transform.getBlockSize()/2);
Chris@752 291 transform.setParameter("serialise", 1);
Chris@752 292 transform.setParameter("smooth", 0);
Chris@752 293 transform.setParameter("zonewidth", 40);
Chris@752 294 transform.setParameter("noise", true);
Chris@752 295 transform.setParameter("minfreq", 500);
Chris@752 296
Chris@752 297 int cents = 0;
Chris@752 298
Chris@752 299 if (m_tuningFrequency != 0.f) {
Chris@752 300 transform.setParameter("freq2", m_tuningFrequency);
Chris@752 301
Chris@752 302 double centsOffset = 0.f;
Chris@752 303 int pitch = Pitch::getPitchForFrequency(m_tuningFrequency,
Chris@752 304 &centsOffset);
Chris@752 305 cents = int(round((pitch - 69) * 100 + centsOffset));
Chris@752 306 SVCERR << "TransformAligner: frequency " << m_tuningFrequency
Chris@752 307 << " yields cents offset " << centsOffset
Chris@752 308 << " and pitch " << pitch << " -> cents " << cents << endl;
Chris@752 309 }
Chris@752 310
Chris@752 311 alignmentModel->setRelativePitch(cents);
Chris@752 312
Chris@753 313 SVDEBUG << "TransformAligner: Alignment transform step size "
Chris@752 314 << transform.getStepSize() << ", block size "
Chris@752 315 << transform.getBlockSize() << endl;
Chris@752 316
Chris@752 317 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
Chris@752 318
Chris@752 319 QString message;
Chris@753 320 m_pathOutputModel = mtf->transform
Chris@752 321 (transform, m_aggregateModel, message);
Chris@752 322
Chris@753 323 if (m_pathOutputModel.isNone()) {
Chris@752 324 transform.setStepSize(0);
Chris@753 325 m_pathOutputModel = mtf->transform
Chris@752 326 (transform, m_aggregateModel, message);
Chris@752 327 }
Chris@752 328
Chris@752 329 auto pathOutputModel =
Chris@753 330 ModelById::getAs<SparseTimeValueModel>(m_pathOutputModel);
Chris@752 331
Chris@752 332 //!!! callers will need to be updated to get error from
Chris@752 333 //!!! alignment model after initial call
Chris@752 334
Chris@752 335 if (!pathOutputModel) {
Chris@752 336 SVCERR << "TransformAligner: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
Chris@752 337 alignmentModel->setError(message);
Chris@752 338 return false;
Chris@752 339 }
Chris@752 340
Chris@752 341 pathOutputModel->setCompletion(0);
Chris@752 342 alignmentModel->setPathFrom(m_pathOutputModel);
Chris@752 343
Chris@769 344 connect(pathOutputModel.get(), SIGNAL(completionChanged(ModelId)),
Chris@752 345 this, SLOT(alignmentCompletionChanged(ModelId)));
Chris@752 346
Chris@752 347 return true;
Chris@752 348 }
Chris@752 349
Chris@752 350 void
Chris@769 351 TransformAligner::alignmentCompletionChanged(ModelId pathOutputModelId)
Chris@752 352 {
Chris@769 353 if (pathOutputModelId != m_pathOutputModel) {
Chris@752 354 SVCERR << "WARNING: TransformAligner::alignmentCompletionChanged: Model "
Chris@769 355 << pathOutputModelId
Chris@753 356 << " is not ours! (ours is "
Chris@769 357 << m_pathOutputModel << ")" << endl;
Chris@752 358 return;
Chris@752 359 }
Chris@752 360
Chris@769 361 auto pathOutputModel =
Chris@769 362 ModelById::getAs<SparseTimeValueModel>(m_pathOutputModel);
Chris@769 363 if (!pathOutputModel) {
Chris@769 364 SVCERR << "WARNING: TransformAligner::alignmentCompletionChanged: Path output model "
Chris@769 365 << m_pathOutputModel << " no longer exists" << endl;
Chris@769 366 return;
Chris@769 367 }
Chris@769 368
Chris@769 369 int completion = 0;
Chris@769 370 bool done = pathOutputModel->isReady(&completion);
Chris@752 371
Chris@769 372 if (m_withTuningDifference) {
Chris@769 373 if (auto alignmentModel =
Chris@769 374 ModelById::getAs<AlignmentModel>(m_alignmentModel)) {
Chris@769 375 if (!done) {
Chris@769 376 int adjustedCompletion = 50 + completion/2;
Chris@769 377 if (adjustedCompletion > 99) {
Chris@769 378 adjustedCompletion = 99;
Chris@769 379 }
Chris@769 380 alignmentModel->setCompletion(adjustedCompletion);
Chris@769 381 } else {
Chris@769 382 alignmentModel->setCompletion(100);
Chris@769 383 }
Chris@769 384 }
Chris@769 385 }
Chris@752 386
Chris@769 387 if (done) {
Chris@752 388 m_incomplete = false;
Chris@752 389
Chris@752 390 ModelById::release(m_pathOutputModel);
Chris@752 391 m_pathOutputModel = {};
Chris@752 392
Chris@752 393 emit complete(m_alignmentModel);
Chris@752 394 }
Chris@752 395 }