annotate framework/Align.cpp @ 718:464fed3096f5

Avoid deadlock when process finishes immediately (so alignmentProgramFinished is called from waitForStarted while mutex already held)
author Chris Cannam
date Thu, 31 Oct 2019 11:28:35 +0000
parents d2e8e9788cd4
children
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@706 407 transform.setParameter("noise", true);
Chris@706 408 transform.setParameter("minfreq", 500);
Chris@420 409
Chris@704 410 int cents = 0;
Chris@704 411
Chris@670 412 if (tuningFrequency != 0.f) {
Chris@670 413 transform.setParameter("freq2", tuningFrequency);
Chris@704 414
Chris@704 415 double centsOffset = 0.f;
Chris@704 416 int pitch = Pitch::getPitchForFrequency(tuningFrequency, &centsOffset);
Chris@704 417 cents = int(round((pitch - 69) * 100 + centsOffset));
Chris@704 418 SVCERR << "frequency " << tuningFrequency << " yields cents offset " << centsOffset << " and pitch " << pitch << " -> cents " << cents << endl;
Chris@670 419 }
Chris@670 420
Chris@704 421 alignmentModel->setRelativePitch(cents);
Chris@704 422
Chris@420 423 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
Chris@420 424
Chris@420 425 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
Chris@420 426
Chris@420 427 QString message;
Chris@691 428 ModelId pathOutputModelId = mtf->transform
Chris@683 429 (transform, aggregateModelId, message);
Chris@420 430
Chris@691 431 if (pathOutputModelId.isNone()) {
Chris@420 432 transform.setStepSize(0);
Chris@691 433 pathOutputModelId = mtf->transform
Chris@683 434 (transform, aggregateModelId, message);
Chris@420 435 }
Chris@420 436
Chris@691 437 auto pathOutputModel =
Chris@691 438 ModelById::getAs<SparseTimeValueModel>(pathOutputModelId);
Chris@420 439
Chris@670 440 //!!! callers will need to be updated to get error from
Chris@670 441 //!!! alignment model after initial call
Chris@670 442
Chris@691 443 if (!pathOutputModel) {
Chris@649 444 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
Chris@670 445 alignmentModel->setError(message);
Chris@420 446 return false;
Chris@420 447 }
Chris@420 448
Chris@691 449 pathOutputModel->setCompletion(0);
Chris@691 450 alignmentModel->setPathFrom(pathOutputModelId);
Chris@691 451
Chris@691 452 m_pendingAlignments[alignmentModelId] = pathOutputModelId;
Chris@420 453
Chris@687 454 connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)),
Chris@687 455 this, SLOT(alignmentCompletionChanged(ModelId)));
Chris@420 456
Chris@420 457 return true;
Chris@420 458 }
Chris@420 459
Chris@428 460 void
Chris@691 461 Align::alignmentCompletionChanged(ModelId alignmentModelId)
Chris@428 462 {
Chris@670 463 QMutexLocker locker (&m_mutex);
Chris@683 464
Chris@691 465 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
Chris@691 466
Chris@691 467 if (alignmentModel && alignmentModel->isReady()) {
Chris@691 468
Chris@691 469 if (m_pendingAlignments.find(alignmentModelId) !=
Chris@691 470 m_pendingAlignments.end()) {
Chris@691 471 ModelId pathOutputModelId = m_pendingAlignments[alignmentModelId];
Chris@691 472 ModelById::release(pathOutputModelId);
Chris@691 473 m_pendingAlignments.erase(alignmentModelId);
Chris@691 474 }
Chris@691 475
Chris@691 476 disconnect(alignmentModel.get(),
Chris@691 477 SIGNAL(completionChanged(ModelId)),
Chris@687 478 this, SLOT(alignmentCompletionChanged(ModelId)));
Chris@691 479 emit alignmentComplete(alignmentModelId);
Chris@428 480 }
Chris@428 481 }
Chris@428 482
Chris@420 483 bool
Chris@691 484 Align::alignModelViaProgram(Document *doc,
Chris@687 485 ModelId referenceId,
Chris@687 486 ModelId otherId,
Chris@687 487 QString program,
Chris@687 488 QString &error)
Chris@420 489 {
Chris@420 490 // Run an external program, passing to it paths to the main
Chris@420 491 // model's audio file and the new model's audio file. It returns
Chris@420 492 // the path in CSV form through stdout.
Chris@420 493
Chris@687 494 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(referenceId);
Chris@687 495 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(otherId);
Chris@687 496 if (!reference || !other) {
Chris@649 497 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
Chris@515 498 return false;
Chris@515 499 }
Chris@687 500
Chris@687 501 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
Chris@687 502 qApp->processEvents();
Chris@687 503 }
Chris@515 504
Chris@687 505 QString refPath = reference->getLocalFilename();
Chris@716 506 if (refPath == "") {
Chris@716 507 refPath = FileSource(reference->getLocation()).getLocalFilename();
Chris@716 508 }
Chris@716 509
Chris@687 510 QString otherPath = other->getLocalFilename();
Chris@716 511 if (otherPath == "") {
Chris@716 512 otherPath = FileSource(other->getLocation()).getLocalFilename();
Chris@716 513 }
Chris@420 514
Chris@420 515 if (refPath == "" || otherPath == "") {
Chris@670 516 error = "Failed to find local filepath for wave-file model";
Chris@595 517 return false;
Chris@420 518 }
Chris@420 519
Chris@718 520 QProcess *process = nullptr;
Chris@718 521 ModelId alignmentModelId = {};
Chris@718 522
Chris@718 523 {
Chris@718 524 QMutexLocker locker (&m_mutex);
Chris@423 525
Chris@718 526 auto alignmentModel =
Chris@718 527 std::make_shared<AlignmentModel>(referenceId, otherId, ModelId());
Chris@718 528
Chris@718 529 alignmentModelId = ModelById::add(alignmentModel);
Chris@718 530 other->setAlignment(alignmentModelId);
Chris@718 531
Chris@718 532 process = new QProcess;
Chris@718 533 process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
Chris@718 534
Chris@718 535 connect(process,
Chris@718 536 SIGNAL(finished(int, QProcess::ExitStatus)),
Chris@718 537 this,
Chris@718 538 SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
Chris@718 539
Chris@718 540 m_pendingProcesses[process] = alignmentModelId;
Chris@718 541 }
Chris@717 542
Chris@420 543 QStringList args;
Chris@420 544 args << refPath << otherPath;
Chris@717 545
Chris@717 546 SVCERR << "Align::alignModelViaProgram: Starting program \""
Chris@717 547 << program << "\" with args: ";
Chris@717 548 for (auto a: args) {
Chris@717 549 SVCERR << "\"" << a << "\" ";
Chris@717 550 }
Chris@717 551 SVCERR << endl;
Chris@717 552
Chris@423 553 process->start(program, args);
Chris@420 554
Chris@423 555 bool success = process->waitForStarted();
Chris@423 556
Chris@718 557 {
Chris@718 558 QMutexLocker locker(&m_mutex);
Chris@718 559
Chris@718 560 if (!success) {
Chris@718 561
Chris@718 562 SVCERR << "ERROR: Align::alignModelViaProgram: "
Chris@718 563 << "Program did not start" << endl;
Chris@718 564 error = "Alignment program \"" + program + "\" did not start";
Chris@718 565
Chris@718 566 m_pendingProcesses.erase(process);
Chris@718 567 other->setAlignment({});
Chris@718 568 ModelById::release(alignmentModelId);
Chris@718 569 delete process;
Chris@718 570
Chris@718 571 } else {
Chris@718 572 doc->addNonDerivedModel(alignmentModelId);
Chris@718 573 }
Chris@423 574 }
Chris@423 575
Chris@423 576 return success;
Chris@423 577 }
Chris@423 578
Chris@423 579 void
Chris@423 580 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
Chris@423 581 {
Chris@670 582 QMutexLocker locker (&m_mutex);
Chris@670 583
Chris@649 584 SVCERR << "Align::alignmentProgramFinished" << endl;
Chris@423 585
Chris@423 586 QProcess *process = qobject_cast<QProcess *>(sender());
Chris@423 587
Chris@670 588 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
Chris@649 589 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
Chris@649 590 << " not found in process model map!" << endl;
Chris@423 591 return;
Chris@423 592 }
Chris@423 593
Chris@683 594 ModelId alignmentModelId = m_pendingProcesses[process];
Chris@683 595 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
Chris@683 596 if (!alignmentModel) return;
Chris@423 597
Chris@423 598 if (exitCode == 0 && status == 0) {
Chris@420 599
Chris@595 600 CSVFormat format;
Chris@595 601 format.setModelType(CSVFormat::TwoDimensionalModel);
Chris@595 602 format.setTimingType(CSVFormat::ExplicitTiming);
Chris@595 603 format.setTimeUnits(CSVFormat::TimeSeconds);
Chris@595 604 format.setColumnCount(2);
Chris@425 605 // The output format has time in the reference file first, and
Chris@425 606 // time in the "other" file in the second column. This is a
Chris@425 607 // more natural approach for a command-line alignment tool,
Chris@425 608 // but it's the opposite of what we expect for native
Chris@425 609 // alignment paths, which map from "other" file to
Chris@425 610 // reference. These column purpose settings reflect that.
Chris@595 611 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
Chris@595 612 format.setColumnPurpose(0, CSVFormat::ColumnValue);
Chris@595 613 format.setAllowQuoting(false);
Chris@595 614 format.setSeparator(',');
Chris@420 615
Chris@595 616 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
Chris@595 617 if (!reader.isOK()) {
Chris@649 618 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
Chris@649 619 << endl;
Chris@670 620 alignmentModel->setError
Chris@670 621 (QString("Failed to parse output of program: %1")
Chris@670 622 .arg(reader.getError()));
Chris@423 623 goto done;
Chris@595 624 }
Chris@420 625
Chris@683 626 //!!! to use ById?
Chris@683 627
Chris@595 628 Model *csvOutput = reader.load();
Chris@420 629
Chris@687 630 SparseTimeValueModel *path =
Chris@687 631 qobject_cast<SparseTimeValueModel *>(csvOutput);
Chris@595 632 if (!path) {
Chris@649 633 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
Chris@649 634 << endl;
Chris@670 635 alignmentModel->setError
Chris@670 636 ("Output of program did not produce sparse time-value model");
Chris@683 637 delete csvOutput;
Chris@423 638 goto done;
Chris@595 639 }
Chris@683 640
Chris@649 641 if (path->isEmpty()) {
Chris@649 642 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
Chris@649 643 << endl;
Chris@670 644 alignmentModel->setError
Chris@670 645 ("Output of alignment program contained no mappings");
Chris@683 646 delete path;
Chris@423 647 goto done;
Chris@595 648 }
Chris@420 649
Chris@649 650 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
Chris@650 651 << path->getEventCount() << " point(s))" << endl;
Chris@650 652
Chris@687 653 auto pathId =
Chris@687 654 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
Chris@687 655 alignmentModel->setPathFrom(pathId);
Chris@420 656
Chris@683 657 emit alignmentComplete(alignmentModelId);
Chris@687 658
Chris@687 659 ModelById::release(pathId);
Chris@428 660
Chris@420 661 } else {
Chris@649 662 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
Chris@649 663 << "failed: exit code " << exitCode << ", status " << status
Chris@649 664 << endl;
Chris@670 665 alignmentModel->setError
Chris@670 666 ("Aligner process returned non-zero exit status");
Chris@420 667 }
Chris@420 668
Chris@423 669 done:
Chris@670 670 m_pendingProcesses.erase(process);
Chris@423 671 delete process;
Chris@420 672 }
Chris@420 673