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, &centsOffset);
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