Mercurial > hg > svapp
comparison align/Align.cpp @ 744:36772d79cf44 pitch-align
Move Align to new align directory
| author | Chris Cannam | 
|---|---|
| date | Fri, 03 Apr 2020 10:17:46 +0100 | 
| parents | framework/Align.cpp@464fed3096f5 | 
| children | 31289e8592c7 | 
   comparison
  equal
  deleted
  inserted
  replaced
| 743:7b1d30af4b38 | 744:36772d79cf44 | 
|---|---|
| 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 "Align.h" | |
| 16 #include "framework/Document.h" | |
| 17 | |
| 18 #include "data/model/WaveFileModel.h" | |
| 19 #include "data/model/ReadOnlyWaveFileModel.h" | |
| 20 #include "data/model/AggregateWaveModel.h" | |
| 21 #include "data/model/RangeSummarisableTimeValueModel.h" | |
| 22 #include "data/model/SparseTimeValueModel.h" | |
| 23 #include "data/model/AlignmentModel.h" | |
| 24 | |
| 25 #include "data/fileio/CSVFileReader.h" | |
| 26 | |
| 27 #include "transform/TransformFactory.h" | |
| 28 #include "transform/ModelTransformerFactory.h" | |
| 29 #include "transform/FeatureExtractionModelTransformer.h" | |
| 30 | |
| 31 #include <QProcess> | |
| 32 #include <QSettings> | |
| 33 #include <QApplication> | |
| 34 | |
| 35 bool | |
| 36 Align::alignModel(Document *doc, ModelId ref, ModelId other, QString &error) | |
| 37 { | |
| 38 QSettings settings; | |
| 39 settings.beginGroup("Preferences"); | |
| 40 bool useProgram = settings.value("use-external-alignment", false).toBool(); | |
| 41 QString program = settings.value("external-alignment-program", "").toString(); | |
| 42 settings.endGroup(); | |
| 43 | |
| 44 if (useProgram && (program != "")) { | |
| 45 return alignModelViaProgram(doc, ref, other, program, error); | |
| 46 } else { | |
| 47 return alignModelViaTransform(doc, ref, other, error); | |
| 48 } | |
| 49 } | |
| 50 | |
| 51 QString | |
| 52 Align::getAlignmentTransformName() | |
| 53 { | |
| 54 QSettings settings; | |
| 55 settings.beginGroup("Alignment"); | |
| 56 TransformId id = | |
| 57 settings.value("transform-id", | |
| 58 "vamp:match-vamp-plugin:match:path").toString(); | |
| 59 settings.endGroup(); | |
| 60 return id; | |
| 61 } | |
| 62 | |
| 63 QString | |
| 64 Align::getTuningDifferenceTransformName() | |
| 65 { | |
| 66 QSettings settings; | |
| 67 settings.beginGroup("Alignment"); | |
| 68 bool performPitchCompensation = | |
| 69 settings.value("align-pitch-aware", false).toBool(); | |
| 70 QString id = ""; | |
| 71 if (performPitchCompensation) { | |
| 72 id = settings.value | |
| 73 ("tuning-difference-transform-id", | |
| 74 "vamp:tuning-difference:tuning-difference:tuningfreq") | |
| 75 .toString(); | |
| 76 } | |
| 77 settings.endGroup(); | |
| 78 return id; | |
| 79 } | |
| 80 | |
| 81 bool | |
| 82 Align::canAlign() | |
| 83 { | |
| 84 TransformFactory *factory = TransformFactory::getInstance(); | |
| 85 TransformId id = getAlignmentTransformName(); | |
| 86 TransformId tdId = getTuningDifferenceTransformName(); | |
| 87 return factory->haveTransform(id) && | |
| 88 (tdId == "" || factory->haveTransform(tdId)); | |
| 89 } | |
| 90 | |
| 91 void | |
| 92 Align::abandonOngoingAlignment(ModelId otherId) | |
| 93 { | |
| 94 auto other = ModelById::getAs<RangeSummarisableTimeValueModel>(otherId); | |
| 95 if (!other) { | |
| 96 return; | |
| 97 } | |
| 98 | |
| 99 ModelId alignmentModelId = other->getAlignment(); | |
| 100 if (alignmentModelId.isNone()) { | |
| 101 return; | |
| 102 } | |
| 103 | |
| 104 SVCERR << "Align::abandonOngoingAlignment: An alignment is ongoing for model " | |
| 105 << otherId << " (alignment model id " << alignmentModelId | |
| 106 << "), abandoning it..." << endl; | |
| 107 | |
| 108 other->setAlignment({}); | |
| 109 | |
| 110 for (auto pp: m_pendingProcesses) { | |
| 111 if (alignmentModelId == pp.second) { | |
| 112 QProcess *process = pp.first; | |
| 113 m_pendingProcesses.erase(process); | |
| 114 SVCERR << "Align::abandonOngoingAlignment: Killing external " | |
| 115 << "alignment process " << process << "..." << endl; | |
| 116 delete process; // kills the process itself | |
| 117 break; | |
| 118 } | |
| 119 } | |
| 120 | |
| 121 if (m_pendingAlignments.find(alignmentModelId) != | |
| 122 m_pendingAlignments.end()) { | |
| 123 SVCERR << "Align::abandonOngoingAlignment: Releasing path output model " | |
| 124 << m_pendingAlignments[alignmentModelId] | |
| 125 << "..." << endl; | |
| 126 ModelById::release(m_pendingAlignments[alignmentModelId]); | |
| 127 SVCERR << "Align::abandonOngoingAlignment: Dropping alignment model " | |
| 128 << alignmentModelId | |
| 129 << " from pending alignments..." << endl; | |
| 130 m_pendingAlignments.erase(alignmentModelId); | |
| 131 } | |
| 132 | |
| 133 for (auto ptd: m_pendingTuningDiffs) { | |
| 134 if (alignmentModelId == ptd.second.alignment) { | |
| 135 SVCERR << "Align::abandonOngoingAlignment: Releasing preparatory model " | |
| 136 << ptd.second.preparatory << "..." << endl; | |
| 137 ModelById::release(ptd.second.preparatory); | |
| 138 SVCERR << "Align::abandonOngoingAlignment: Releasing pending tuning-diff model " | |
| 139 << ptd.first << "..." << endl; | |
| 140 ModelById::release(ptd.first); | |
| 141 SVCERR << "Align::abandonOngoingAlignment: Dropping tuning-diff model " | |
| 142 << ptd.first | |
| 143 << " from pending tuning diffs..." << endl; | |
| 144 m_pendingTuningDiffs.erase(ptd.first); | |
| 145 break; | |
| 146 } | |
| 147 } | |
| 148 | |
| 149 SVCERR << "Align::abandonOngoingAlignment: done" << endl; | |
| 150 } | |
| 151 | |
| 152 bool | |
| 153 Align::alignModelViaTransform(Document *doc, | |
| 154 ModelId referenceId, | |
| 155 ModelId otherId, | |
| 156 QString &error) | |
| 157 { | |
| 158 QMutexLocker locker (&m_mutex); | |
| 159 | |
| 160 auto reference = | |
| 161 ModelById::getAs<RangeSummarisableTimeValueModel>(referenceId); | |
| 162 auto other = | |
| 163 ModelById::getAs<RangeSummarisableTimeValueModel>(otherId); | |
| 164 | |
| 165 if (!reference || !other) return false; | |
| 166 | |
| 167 // There may be an alignment already happening; we should stop it, | |
| 168 // which we can do by discarding the output models for its | |
| 169 // transforms | |
| 170 abandonOngoingAlignment(otherId); | |
| 171 | |
| 172 // This involves creating a number of new models: | |
| 173 // | |
| 174 // 1. an AggregateWaveModel to provide the mixdowns of the main | |
| 175 // model and the new model in its two channels, as input to the | |
| 176 // MATCH plugin. We just call this one aggregateModel | |
| 177 // | |
| 178 // 2a. a SparseTimeValueModel which will be automatically created | |
| 179 // by FeatureExtractionModelTransformer when running the | |
| 180 // TuningDifference plugin to receive the relative tuning of the | |
| 181 // second model (if pitch-aware alignment is enabled in the | |
| 182 // preferences). We call this tuningDiffOutputModel. | |
| 183 // | |
| 184 // 2b. a SparseTimeValueModel which will be automatically created | |
| 185 // by FeatureExtractionPluginTransformer when running the MATCH | |
| 186 // plugin to perform alignment (so containing the alignment path). | |
| 187 // We call this one pathOutputModel. | |
| 188 // | |
| 189 // 2c. a SparseTimeValueModel used solely to provide faked | |
| 190 // completion information to the AlignmentModel while a | |
| 191 // TuningDifference calculation is going on. We call this | |
| 192 // preparatoryModel. | |
| 193 // | |
| 194 // 3. an AlignmentModel, which stores the path and carries out | |
| 195 // alignment lookups on it. We just call this one alignmentModel. | |
| 196 // | |
| 197 // Models 1 and 3 are registered with the document, which will | |
| 198 // eventually release them. We don't release them here except in | |
| 199 // the case where an activity fails before the point where we | |
| 200 // would otherwise have registered them with the document. | |
| 201 // | |
| 202 // Models 2a (tuningDiffOutputModel), 2b (pathOutputModel) and 2c | |
| 203 // (preparatoryModel) are not registered with the document. Model | |
| 204 // 2b (pathOutputModel) is not registered because we do not have a | |
| 205 // stable reference to the document at the point where it is | |
| 206 // created. Model 2c (preparatoryModel) is not registered because | |
| 207 // it is a bodge that we are embarrassed about, so we try to | |
| 208 // manage it ourselves without anyone else noticing. Model 2a is | |
| 209 // not registered for symmetry with the other two. These have to | |
| 210 // be released by us when finished with, but their lifespans do | |
| 211 // not extend beyond the end of the alignment procedure, so this | |
| 212 // should be ok. | |
| 213 | |
| 214 AggregateWaveModel::ChannelSpecList components; | |
| 215 | |
| 216 components.push_back | |
| 217 (AggregateWaveModel::ModelChannelSpec(referenceId, -1)); | |
| 218 | |
| 219 components.push_back | |
| 220 (AggregateWaveModel::ModelChannelSpec(otherId, -1)); | |
| 221 | |
| 222 auto aggregateModel = std::make_shared<AggregateWaveModel>(components); | |
| 223 auto aggregateModelId = ModelById::add(aggregateModel); | |
| 224 doc->addNonDerivedModel(aggregateModelId); | |
| 225 | |
| 226 auto alignmentModel = std::make_shared<AlignmentModel> | |
| 227 (referenceId, otherId, ModelId()); | |
| 228 auto alignmentModelId = ModelById::add(alignmentModel); | |
| 229 | |
| 230 TransformId tdId = getTuningDifferenceTransformName(); | |
| 231 | |
| 232 if (tdId == "") { | |
| 233 | |
| 234 if (beginTransformDrivenAlignment(aggregateModelId, | |
| 235 alignmentModelId)) { | |
| 236 other->setAlignment(alignmentModelId); | |
| 237 doc->addNonDerivedModel(alignmentModelId); | |
| 238 } else { | |
| 239 error = alignmentModel->getError(); | |
| 240 ModelById::release(alignmentModel); | |
| 241 return false; | |
| 242 } | |
| 243 | |
| 244 } else { | |
| 245 | |
| 246 // Have a tuning-difference transform id, so run it | |
| 247 // asynchronously first | |
| 248 | |
| 249 TransformFactory *tf = TransformFactory::getInstance(); | |
| 250 | |
| 251 Transform transform = tf->getDefaultTransformFor | |
| 252 (tdId, aggregateModel->getSampleRate()); | |
| 253 | |
| 254 transform.setParameter("maxduration", 60); | |
| 255 transform.setParameter("maxrange", 6); | |
| 256 transform.setParameter("finetuning", false); | |
| 257 | |
| 258 SVDEBUG << "Align::alignModel: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; | |
| 259 | |
| 260 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); | |
| 261 | |
| 262 QString message; | |
| 263 ModelId tuningDiffOutputModelId = mtf->transform(transform, | |
| 264 aggregateModelId, | |
| 265 message); | |
| 266 | |
| 267 auto tuningDiffOutputModel = | |
| 268 ModelById::getAs<SparseTimeValueModel>(tuningDiffOutputModelId); | |
| 269 if (!tuningDiffOutputModel) { | |
| 270 SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl; | |
| 271 error = message; | |
| 272 ModelById::release(alignmentModel); | |
| 273 return false; | |
| 274 } | |
| 275 | |
| 276 other->setAlignment(alignmentModelId); | |
| 277 doc->addNonDerivedModel(alignmentModelId); | |
| 278 | |
| 279 connect(tuningDiffOutputModel.get(), | |
| 280 SIGNAL(completionChanged(ModelId)), | |
| 281 this, SLOT(tuningDifferenceCompletionChanged(ModelId))); | |
| 282 | |
| 283 TuningDiffRec rec; | |
| 284 rec.input = aggregateModelId; | |
| 285 rec.alignment = alignmentModelId; | |
| 286 | |
| 287 // This model exists only so that the AlignmentModel can get a | |
| 288 // completion value from somewhere while the tuning difference | |
| 289 // calculation is going on | |
| 290 auto preparatoryModel = std::make_shared<SparseTimeValueModel> | |
| 291 (aggregateModel->getSampleRate(), 1); | |
| 292 auto preparatoryModelId = ModelById::add(preparatoryModel); | |
| 293 preparatoryModel->setCompletion(0); | |
| 294 rec.preparatory = preparatoryModelId; | |
| 295 alignmentModel->setPathFrom(rec.preparatory); | |
| 296 | |
| 297 m_pendingTuningDiffs[tuningDiffOutputModelId] = rec; | |
| 298 | |
| 299 SVDEBUG << "Align::alignModelViaTransform: Made a note of pending tuning diff output model id " << tuningDiffOutputModelId << " with input " << rec.input << ", alignment model " << rec.alignment << ", preparatory model " << rec.preparatory << endl; | |
| 300 } | |
| 301 | |
| 302 return true; | |
| 303 } | |
| 304 | |
| 305 void | |
| 306 Align::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId) | |
| 307 { | |
| 308 QMutexLocker locker(&m_mutex); | |
| 309 | |
| 310 if (m_pendingTuningDiffs.find(tuningDiffOutputModelId) == | |
| 311 m_pendingTuningDiffs.end()) { | |
| 312 SVDEBUG << "NOTE: Align::tuningDifferenceCompletionChanged: Model " | |
| 313 << tuningDiffOutputModelId | |
| 314 << " not found in pending tuning diff map, presuming " | |
| 315 << "completed or abandoned" << endl; | |
| 316 return; | |
| 317 } | |
| 318 | |
| 319 auto tuningDiffOutputModel = | |
| 320 ModelById::getAs<SparseTimeValueModel>(tuningDiffOutputModelId); | |
| 321 if (!tuningDiffOutputModel) { | |
| 322 SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged: Model " | |
| 323 << tuningDiffOutputModelId | |
| 324 << " not known as SparseTimeValueModel" << endl; | |
| 325 return; | |
| 326 } | |
| 327 | |
| 328 TuningDiffRec rec = m_pendingTuningDiffs[tuningDiffOutputModelId]; | |
| 329 | |
| 330 auto alignmentModel = ModelById::getAs<AlignmentModel>(rec.alignment); | |
| 331 if (!alignmentModel) { | |
| 332 SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged:" | |
| 333 << "alignment model has disappeared" << endl; | |
| 334 return; | |
| 335 } | |
| 336 | |
| 337 int completion = 0; | |
| 338 bool done = tuningDiffOutputModel->isReady(&completion); | |
| 339 | |
| 340 if (!done) { | |
| 341 // This will be the completion the alignment model reports, | |
| 342 // before the alignment actually begins. It goes up from 0 to | |
| 343 // 99 (not 100!) and then back to 0 again when we start | |
| 344 // calculating the actual path in the following phase | |
| 345 int clamped = (completion == 100 ? 99 : completion); | |
| 346 auto preparatoryModel = | |
| 347 ModelById::getAs<SparseTimeValueModel>(rec.preparatory); | |
| 348 if (preparatoryModel) { | |
| 349 preparatoryModel->setCompletion(clamped); | |
| 350 } | |
| 351 return; | |
| 352 } | |
| 353 | |
| 354 float tuningFrequency = 440.f; | |
| 355 | |
| 356 if (!tuningDiffOutputModel->isEmpty()) { | |
| 357 tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue(); | |
| 358 SVCERR << "Align::tuningDifferenceCompletionChanged: Reported tuning frequency = " << tuningFrequency << endl; | |
| 359 } else { | |
| 360 SVCERR << "Align::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl; | |
| 361 } | |
| 362 | |
| 363 ModelById::release(tuningDiffOutputModel); | |
| 364 | |
| 365 alignmentModel->setPathFrom({}); // replace preparatoryModel | |
| 366 ModelById::release(rec.preparatory); | |
| 367 rec.preparatory = {}; | |
| 368 | |
| 369 m_pendingTuningDiffs.erase(tuningDiffOutputModelId); | |
| 370 | |
| 371 SVDEBUG << "Align::tuningDifferenceCompletionChanged: Erasing model " | |
| 372 << tuningDiffOutputModelId << " from pending tuning diffs and " | |
| 373 << "launching the alignment phase for alignment model " | |
| 374 << rec.alignment << " with tuning frequency " | |
| 375 << tuningFrequency << endl; | |
| 376 | |
| 377 beginTransformDrivenAlignment | |
| 378 (rec.input, rec.alignment, tuningFrequency); | |
| 379 } | |
| 380 | |
| 381 bool | |
| 382 Align::beginTransformDrivenAlignment(ModelId aggregateModelId, | |
| 383 ModelId alignmentModelId, | |
| 384 float tuningFrequency) | |
| 385 { | |
| 386 TransformId id = getAlignmentTransformName(); | |
| 387 | |
| 388 TransformFactory *tf = TransformFactory::getInstance(); | |
| 389 | |
| 390 auto aggregateModel = | |
| 391 ModelById::getAs<AggregateWaveModel>(aggregateModelId); | |
| 392 auto alignmentModel = | |
| 393 ModelById::getAs<AlignmentModel>(alignmentModelId); | |
| 394 | |
| 395 if (!aggregateModel || !alignmentModel) { | |
| 396 SVCERR << "Align::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl; | |
| 397 return false; | |
| 398 } | |
| 399 | |
| 400 Transform transform = tf->getDefaultTransformFor | |
| 401 (id, aggregateModel->getSampleRate()); | |
| 402 | |
| 403 transform.setStepSize(transform.getBlockSize()/2); | |
| 404 transform.setParameter("serialise", 1); | |
| 405 transform.setParameter("smooth", 0); | |
| 406 transform.setParameter("zonewidth", 40); | |
| 407 transform.setParameter("noise", true); | |
| 408 transform.setParameter("minfreq", 500); | |
| 409 | |
| 410 int cents = 0; | |
| 411 | |
| 412 if (tuningFrequency != 0.f) { | |
| 413 transform.setParameter("freq2", tuningFrequency); | |
| 414 | |
| 415 double centsOffset = 0.f; | |
| 416 int pitch = Pitch::getPitchForFrequency(tuningFrequency, ¢sOffset); | |
| 417 cents = int(round((pitch - 69) * 100 + centsOffset)); | |
| 418 SVCERR << "frequency " << tuningFrequency << " yields cents offset " << centsOffset << " and pitch " << pitch << " -> cents " << cents << endl; | |
| 419 } | |
| 420 | |
| 421 alignmentModel->setRelativePitch(cents); | |
| 422 | |
| 423 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; | |
| 424 | |
| 425 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); | |
| 426 | |
| 427 QString message; | |
| 428 ModelId pathOutputModelId = mtf->transform | |
| 429 (transform, aggregateModelId, message); | |
| 430 | |
| 431 if (pathOutputModelId.isNone()) { | |
| 432 transform.setStepSize(0); | |
| 433 pathOutputModelId = mtf->transform | |
| 434 (transform, aggregateModelId, message); | |
| 435 } | |
| 436 | |
| 437 auto pathOutputModel = | |
| 438 ModelById::getAs<SparseTimeValueModel>(pathOutputModelId); | |
| 439 | |
| 440 //!!! callers will need to be updated to get error from | |
| 441 //!!! alignment model after initial call | |
| 442 | |
| 443 if (!pathOutputModel) { | |
| 444 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl; | |
| 445 alignmentModel->setError(message); | |
| 446 return false; | |
| 447 } | |
| 448 | |
| 449 pathOutputModel->setCompletion(0); | |
| 450 alignmentModel->setPathFrom(pathOutputModelId); | |
| 451 | |
| 452 m_pendingAlignments[alignmentModelId] = pathOutputModelId; | |
| 453 | |
| 454 connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)), | |
| 455 this, SLOT(alignmentCompletionChanged(ModelId))); | |
| 456 | |
| 457 return true; | |
| 458 } | |
| 459 | |
| 460 void | |
| 461 Align::alignmentCompletionChanged(ModelId alignmentModelId) | |
| 462 { | |
| 463 QMutexLocker locker (&m_mutex); | |
| 464 | |
| 465 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId); | |
| 466 | |
| 467 if (alignmentModel && alignmentModel->isReady()) { | |
| 468 | |
| 469 if (m_pendingAlignments.find(alignmentModelId) != | |
| 470 m_pendingAlignments.end()) { | |
| 471 ModelId pathOutputModelId = m_pendingAlignments[alignmentModelId]; | |
| 472 ModelById::release(pathOutputModelId); | |
| 473 m_pendingAlignments.erase(alignmentModelId); | |
| 474 } | |
| 475 | |
| 476 disconnect(alignmentModel.get(), | |
| 477 SIGNAL(completionChanged(ModelId)), | |
| 478 this, SLOT(alignmentCompletionChanged(ModelId))); | |
| 479 emit alignmentComplete(alignmentModelId); | |
| 480 } | |
| 481 } | |
| 482 | |
| 483 bool | |
| 484 Align::alignModelViaProgram(Document *doc, | |
| 485 ModelId referenceId, | |
| 486 ModelId otherId, | |
| 487 QString program, | |
| 488 QString &error) | |
| 489 { | |
| 490 // Run an external program, passing to it paths to the main | |
| 491 // model's audio file and the new model's audio file. It returns | |
| 492 // the path in CSV form through stdout. | |
| 493 | |
| 494 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(referenceId); | |
| 495 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(otherId); | |
| 496 if (!reference || !other) { | |
| 497 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl; | |
| 498 return false; | |
| 499 } | |
| 500 | |
| 501 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) { | |
| 502 qApp->processEvents(); | |
| 503 } | |
| 504 | |
| 505 QString refPath = reference->getLocalFilename(); | |
| 506 if (refPath == "") { | |
| 507 refPath = FileSource(reference->getLocation()).getLocalFilename(); | |
| 508 } | |
| 509 | |
| 510 QString otherPath = other->getLocalFilename(); | |
| 511 if (otherPath == "") { | |
| 512 otherPath = FileSource(other->getLocation()).getLocalFilename(); | |
| 513 } | |
| 514 | |
| 515 if (refPath == "" || otherPath == "") { | |
| 516 error = "Failed to find local filepath for wave-file model"; | |
| 517 return false; | |
| 518 } | |
| 519 | |
| 520 QProcess *process = nullptr; | |
| 521 ModelId alignmentModelId = {}; | |
| 522 | |
| 523 { | |
| 524 QMutexLocker locker (&m_mutex); | |
| 525 | |
| 526 auto alignmentModel = | |
| 527 std::make_shared<AlignmentModel>(referenceId, otherId, ModelId()); | |
| 528 | |
| 529 alignmentModelId = ModelById::add(alignmentModel); | |
| 530 other->setAlignment(alignmentModelId); | |
| 531 | |
| 532 process = new QProcess; | |
| 533 process->setProcessChannelMode(QProcess::ForwardedErrorChannel); | |
| 534 | |
| 535 connect(process, | |
| 536 SIGNAL(finished(int, QProcess::ExitStatus)), | |
| 537 this, | |
| 538 SLOT(alignmentProgramFinished(int, QProcess::ExitStatus))); | |
| 539 | |
| 540 m_pendingProcesses[process] = alignmentModelId; | |
| 541 } | |
| 542 | |
| 543 QStringList args; | |
| 544 args << refPath << otherPath; | |
| 545 | |
| 546 SVCERR << "Align::alignModelViaProgram: Starting program \"" | |
| 547 << program << "\" with args: "; | |
| 548 for (auto a: args) { | |
| 549 SVCERR << "\"" << a << "\" "; | |
| 550 } | |
| 551 SVCERR << endl; | |
| 552 | |
| 553 process->start(program, args); | |
| 554 | |
| 555 bool success = process->waitForStarted(); | |
| 556 | |
| 557 { | |
| 558 QMutexLocker locker(&m_mutex); | |
| 559 | |
| 560 if (!success) { | |
| 561 | |
| 562 SVCERR << "ERROR: Align::alignModelViaProgram: " | |
| 563 << "Program did not start" << endl; | |
| 564 error = "Alignment program \"" + program + "\" did not start"; | |
| 565 | |
| 566 m_pendingProcesses.erase(process); | |
| 567 other->setAlignment({}); | |
| 568 ModelById::release(alignmentModelId); | |
| 569 delete process; | |
| 570 | |
| 571 } else { | |
| 572 doc->addNonDerivedModel(alignmentModelId); | |
| 573 } | |
| 574 } | |
| 575 | |
| 576 return success; | |
| 577 } | |
| 578 | |
| 579 void | |
| 580 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status) | |
| 581 { | |
| 582 QMutexLocker locker (&m_mutex); | |
| 583 | |
| 584 SVCERR << "Align::alignmentProgramFinished" << endl; | |
| 585 | |
| 586 QProcess *process = qobject_cast<QProcess *>(sender()); | |
| 587 | |
| 588 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) { | |
| 589 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process | |
| 590 << " not found in process model map!" << endl; | |
| 591 return; | |
| 592 } | |
| 593 | |
| 594 ModelId alignmentModelId = m_pendingProcesses[process]; | |
| 595 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId); | |
| 596 if (!alignmentModel) return; | |
| 597 | |
| 598 if (exitCode == 0 && status == 0) { | |
| 599 | |
| 600 CSVFormat format; | |
| 601 format.setModelType(CSVFormat::TwoDimensionalModel); | |
| 602 format.setTimingType(CSVFormat::ExplicitTiming); | |
| 603 format.setTimeUnits(CSVFormat::TimeSeconds); | |
| 604 format.setColumnCount(2); | |
| 605 // The output format has time in the reference file first, and | |
| 606 // time in the "other" file in the second column. This is a | |
| 607 // more natural approach for a command-line alignment tool, | |
| 608 // but it's the opposite of what we expect for native | |
| 609 // alignment paths, which map from "other" file to | |
| 610 // reference. These column purpose settings reflect that. | |
| 611 format.setColumnPurpose(1, CSVFormat::ColumnStartTime); | |
| 612 format.setColumnPurpose(0, CSVFormat::ColumnValue); | |
| 613 format.setAllowQuoting(false); | |
| 614 format.setSeparator(','); | |
| 615 | |
| 616 CSVFileReader reader(process, format, alignmentModel->getSampleRate()); | |
| 617 if (!reader.isOK()) { | |
| 618 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output" | |
| 619 << endl; | |
| 620 alignmentModel->setError | |
| 621 (QString("Failed to parse output of program: %1") | |
| 622 .arg(reader.getError())); | |
| 623 goto done; | |
| 624 } | |
| 625 | |
| 626 //!!! to use ById? | |
| 627 | |
| 628 Model *csvOutput = reader.load(); | |
| 629 | |
| 630 SparseTimeValueModel *path = | |
| 631 qobject_cast<SparseTimeValueModel *>(csvOutput); | |
| 632 if (!path) { | |
| 633 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model" | |
| 634 << endl; | |
| 635 alignmentModel->setError | |
| 636 ("Output of program did not produce sparse time-value model"); | |
| 637 delete csvOutput; | |
| 638 goto done; | |
| 639 } | |
| 640 | |
| 641 if (path->isEmpty()) { | |
| 642 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings" | |
| 643 << endl; | |
| 644 alignmentModel->setError | |
| 645 ("Output of alignment program contained no mappings"); | |
| 646 delete path; | |
| 647 goto done; | |
| 648 } | |
| 649 | |
| 650 SVCERR << "Align::alignmentProgramFinished: Setting alignment path (" | |
| 651 << path->getEventCount() << " point(s))" << endl; | |
| 652 | |
| 653 auto pathId = | |
| 654 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path)); | |
| 655 alignmentModel->setPathFrom(pathId); | |
| 656 | |
| 657 emit alignmentComplete(alignmentModelId); | |
| 658 | |
| 659 ModelById::release(pathId); | |
| 660 | |
| 661 } else { | |
| 662 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program " | |
| 663 << "failed: exit code " << exitCode << ", status " << status | |
| 664 << endl; | |
| 665 alignmentModel->setError | |
| 666 ("Aligner process returned non-zero exit status"); | |
| 667 } | |
| 668 | |
| 669 done: | |
| 670 m_pendingProcesses.erase(process); | |
| 671 delete process; | |
| 672 } | |
| 673 | 
