comparison align/Align.cpp @ 753:31289e8592c7 pitch-align

Switch to using the pulled-out TransformAligner and ExternalProgramAligner
author Chris Cannam
date Fri, 24 Apr 2020 14:38:22 +0100
parents 36772d79cf44
children 39808338e771
comparison
equal deleted inserted replaced
752:32654e402f8b 753:31289e8592c7
11 License, or (at your option) any later version. See the file 11 License, or (at your option) any later version. See the file
12 COPYING included with this distribution for more information. 12 COPYING included with this distribution for more information.
13 */ 13 */
14 14
15 #include "Align.h" 15 #include "Align.h"
16 #include "TransformAligner.h"
17 #include "ExternalProgramAligner.h"
16 #include "framework/Document.h" 18 #include "framework/Document.h"
17 19
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> 20 #include <QSettings>
33 #include <QApplication>
34 21
35 bool 22 bool
36 Align::alignModel(Document *doc, ModelId ref, ModelId other, QString &error) 23 Align::alignModel(Document *doc,
24 ModelId reference,
25 ModelId toAlign,
26 QString &error)
27 {
28 bool useProgram;
29 QString program;
30 getAlignerPreference(useProgram, program);
31
32 std::shared_ptr<Aligner> aligner;
33
34 {
35 // Replace the aligner with a new one. This also stops any
36 // previously-running alignment, when the old entry is
37 // replaced and its aligner destroyed.
38
39 QMutexLocker locker(&m_mutex);
40
41 if (useProgram && (program != "")) {
42 m_aligners[toAlign] =
43 std::make_shared<ExternalProgramAligner>(doc,
44 reference,
45 toAlign,
46 program);
47 } else {
48 m_aligners[toAlign] =
49 std::make_shared<TransformAligner>(doc,
50 reference,
51 toAlign);
52 }
53
54 aligner = m_aligners[toAlign];
55 }
56
57 connect(aligner.get(), SIGNAL(complete(ModelId)),
58 this, SLOT(alignerComplete(ModelId)));
59
60 return aligner->begin(error);
61 }
62
63 void
64 Align::getAlignerPreference(bool useProgram, QString program)
37 { 65 {
38 QSettings settings; 66 QSettings settings;
39 settings.beginGroup("Preferences"); 67 settings.beginGroup("Preferences");
40 bool useProgram = settings.value("use-external-alignment", false).toBool(); 68 useProgram = settings.value("use-external-alignment", false).toBool();
41 QString program = settings.value("external-alignment-program", "").toString(); 69 program = settings.value("external-alignment-program", "").toString();
42 settings.endGroup(); 70 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 } 71 }
80 72
81 bool 73 bool
82 Align::canAlign() 74 Align::canAlign()
83 { 75 {
84 TransformFactory *factory = TransformFactory::getInstance(); 76 bool useProgram;
85 TransformId id = getAlignmentTransformName(); 77 QString program;
86 TransformId tdId = getTuningDifferenceTransformName(); 78 getAlignerPreference(useProgram, program);
87 return factory->haveTransform(id) && 79
88 (tdId == "" || factory->haveTransform(tdId)); 80 if (useProgram) {
81 return ExternalProgramAligner::isAvailable(program);
82 } else {
83 return TransformAligner::isAvailable();
84 }
89 } 85 }
90 86
91 void 87 void
92 Align::abandonOngoingAlignment(ModelId otherId) 88 Align::alignerComplete(ModelId alignmentModel)
93 { 89 {
94 auto other = ModelById::getAs<RangeSummarisableTimeValueModel>(otherId); 90 Aligner *aligner = qobject_cast<Aligner *>(sender());
95 if (!other) { 91 if (!aligner) {
92 SVCERR << "ERROR: Align::alignerComplete: Caller is not an Aligner"
93 << endl;
96 return; 94 return;
97 } 95 }
98 96
99 ModelId alignmentModelId = other->getAlignment(); 97 {
100 if (alignmentModelId.isNone()) { 98 QMutexLocker locker (&m_mutex);
101 return;
102 }
103 99
104 SVCERR << "Align::abandonOngoingAlignment: An alignment is ongoing for model " 100 for (auto p: m_aligners) {
105 << otherId << " (alignment model id " << alignmentModelId 101 if (aligner == p.second.get()) {
106 << "), abandoning it..." << endl; 102 m_aligners.erase(p.first);
107 103 break;
108 other->setAlignment({}); 104 }
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 } 105 }
119 } 106 }
120 107
121 if (m_pendingAlignments.find(alignmentModelId) != 108 emit alignmentComplete(alignmentModel);
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 } 109 }
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