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