annotate framework/Align.cpp @ 716:604393795ee5

If no local filename (e.g. when decoded to memory), get the local filename from the file source (e.g. cached version of encoded file)
author Chris Cannam
date Tue, 29 Oct 2019 12:47:44 +0000
parents 162c3f4b870c
children d2e8e9788cd4
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@670 490 QMutexLocker locker (&m_mutex);
Chris@430 491
Chris@420 492 // Run an external program, passing to it paths to the main
Chris@420 493 // model's audio file and the new model's audio file. It returns
Chris@420 494 // the path in CSV form through stdout.
Chris@420 495
Chris@687 496 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(referenceId);
Chris@687 497 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(otherId);
Chris@687 498 if (!reference || !other) {
Chris@649 499 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
Chris@515 500 return false;
Chris@515 501 }
Chris@687 502
Chris@687 503 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
Chris@687 504 qApp->processEvents();
Chris@687 505 }
Chris@515 506
Chris@687 507 QString refPath = reference->getLocalFilename();
Chris@716 508 if (refPath == "") {
Chris@716 509 refPath = FileSource(reference->getLocation()).getLocalFilename();
Chris@716 510 }
Chris@716 511
Chris@687 512 QString otherPath = other->getLocalFilename();
Chris@716 513 if (otherPath == "") {
Chris@716 514 otherPath = FileSource(other->getLocation()).getLocalFilename();
Chris@716 515 }
Chris@420 516
Chris@420 517 if (refPath == "" || otherPath == "") {
Chris@670 518 error = "Failed to find local filepath for wave-file model";
Chris@595 519 return false;
Chris@420 520 }
Chris@420 521
Chris@687 522 auto alignmentModel =
Chris@687 523 std::make_shared<AlignmentModel>(referenceId, otherId, ModelId());
Chris@687 524 auto alignmentModelId = ModelById::add(alignmentModel);
Chris@687 525 other->setAlignment(alignmentModelId);
Chris@423 526
Chris@423 527 QProcess *process = new QProcess;
Chris@420 528 QStringList args;
Chris@420 529 args << refPath << otherPath;
Chris@423 530
Chris@423 531 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
Chris@423 532 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
Chris@420 533
Chris@687 534 m_pendingProcesses[process] = alignmentModelId;
Chris@423 535 process->start(program, args);
Chris@420 536
Chris@423 537 bool success = process->waitForStarted();
Chris@423 538
Chris@423 539 if (!success) {
Chris@649 540 SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
Chris@649 541 << endl;
Chris@670 542 error = "Alignment program could not be started";
Chris@670 543 m_pendingProcesses.erase(process);
Chris@687 544 other->setAlignment({});
Chris@691 545 ModelById::release(alignmentModelId);
Chris@423 546 delete process;
Chris@423 547 }
Chris@423 548
Chris@691 549 doc->addNonDerivedModel(alignmentModelId);
Chris@423 550 return success;
Chris@423 551 }
Chris@423 552
Chris@423 553 void
Chris@423 554 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
Chris@423 555 {
Chris@670 556 QMutexLocker locker (&m_mutex);
Chris@670 557
Chris@649 558 SVCERR << "Align::alignmentProgramFinished" << endl;
Chris@423 559
Chris@423 560 QProcess *process = qobject_cast<QProcess *>(sender());
Chris@423 561
Chris@670 562 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
Chris@649 563 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
Chris@649 564 << " not found in process model map!" << endl;
Chris@423 565 return;
Chris@423 566 }
Chris@423 567
Chris@683 568 ModelId alignmentModelId = m_pendingProcesses[process];
Chris@683 569 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
Chris@683 570 if (!alignmentModel) return;
Chris@423 571
Chris@423 572 if (exitCode == 0 && status == 0) {
Chris@420 573
Chris@595 574 CSVFormat format;
Chris@595 575 format.setModelType(CSVFormat::TwoDimensionalModel);
Chris@595 576 format.setTimingType(CSVFormat::ExplicitTiming);
Chris@595 577 format.setTimeUnits(CSVFormat::TimeSeconds);
Chris@595 578 format.setColumnCount(2);
Chris@425 579 // The output format has time in the reference file first, and
Chris@425 580 // time in the "other" file in the second column. This is a
Chris@425 581 // more natural approach for a command-line alignment tool,
Chris@425 582 // but it's the opposite of what we expect for native
Chris@425 583 // alignment paths, which map from "other" file to
Chris@425 584 // reference. These column purpose settings reflect that.
Chris@595 585 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
Chris@595 586 format.setColumnPurpose(0, CSVFormat::ColumnValue);
Chris@595 587 format.setAllowQuoting(false);
Chris@595 588 format.setSeparator(',');
Chris@420 589
Chris@595 590 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
Chris@595 591 if (!reader.isOK()) {
Chris@649 592 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
Chris@649 593 << endl;
Chris@670 594 alignmentModel->setError
Chris@670 595 (QString("Failed to parse output of program: %1")
Chris@670 596 .arg(reader.getError()));
Chris@423 597 goto done;
Chris@595 598 }
Chris@420 599
Chris@683 600 //!!! to use ById?
Chris@683 601
Chris@595 602 Model *csvOutput = reader.load();
Chris@420 603
Chris@687 604 SparseTimeValueModel *path =
Chris@687 605 qobject_cast<SparseTimeValueModel *>(csvOutput);
Chris@595 606 if (!path) {
Chris@649 607 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
Chris@649 608 << endl;
Chris@670 609 alignmentModel->setError
Chris@670 610 ("Output of program did not produce sparse time-value model");
Chris@683 611 delete csvOutput;
Chris@423 612 goto done;
Chris@595 613 }
Chris@683 614
Chris@649 615 if (path->isEmpty()) {
Chris@649 616 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
Chris@649 617 << endl;
Chris@670 618 alignmentModel->setError
Chris@670 619 ("Output of alignment program contained no mappings");
Chris@683 620 delete path;
Chris@423 621 goto done;
Chris@595 622 }
Chris@420 623
Chris@649 624 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
Chris@650 625 << path->getEventCount() << " point(s))" << endl;
Chris@650 626
Chris@687 627 auto pathId =
Chris@687 628 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
Chris@687 629 alignmentModel->setPathFrom(pathId);
Chris@420 630
Chris@683 631 emit alignmentComplete(alignmentModelId);
Chris@687 632
Chris@687 633 ModelById::release(pathId);
Chris@428 634
Chris@420 635 } else {
Chris@649 636 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
Chris@649 637 << "failed: exit code " << exitCode << ", status " << status
Chris@649 638 << endl;
Chris@670 639 alignmentModel->setError
Chris@670 640 ("Aligner process returned non-zero exit status");
Chris@420 641 }
Chris@420 642
Chris@423 643 done:
Chris@670 644 m_pendingProcesses.erase(process);
Chris@423 645 delete process;
Chris@420 646 }
Chris@420 647