annotate framework/Align.cpp @ 704:286bd8bb13cc

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