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@420
|
91 bool
|
Chris@687
|
92 Align::alignModelViaTransform(Document *doc,
|
Chris@687
|
93 ModelId referenceId,
|
Chris@687
|
94 ModelId otherId,
|
Chris@670
|
95 QString &error)
|
Chris@420
|
96 {
|
Chris@670
|
97 QMutexLocker locker (&m_mutex);
|
Chris@420
|
98
|
Chris@687
|
99 auto reference =
|
Chris@687
|
100 ModelById::getAs<RangeSummarisableTimeValueModel>(referenceId);
|
Chris@687
|
101 auto other =
|
Chris@687
|
102 ModelById::getAs<RangeSummarisableTimeValueModel>(otherId);
|
Chris@687
|
103
|
Chris@687
|
104 if (!reference || !other) return false;
|
Chris@420
|
105
|
Chris@691
|
106 // This involves creating a number of new models:
|
Chris@672
|
107 //
|
Chris@420
|
108 // 1. an AggregateWaveModel to provide the mixdowns of the main
|
Chris@420
|
109 // model and the new model in its two channels, as input to the
|
Chris@691
|
110 // MATCH plugin. We just call this one aggregateModel
|
Chris@672
|
111 //
|
Chris@670
|
112 // 2a. a SparseTimeValueModel which will be automatically created
|
Chris@670
|
113 // by FeatureExtractionModelTransformer when running the
|
Chris@670
|
114 // TuningDifference plugin to receive the relative tuning of the
|
Chris@670
|
115 // second model (if pitch-aware alignment is enabled in the
|
Chris@691
|
116 // preferences). We call this tuningDiffOutputModel.
|
Chris@672
|
117 //
|
Chris@670
|
118 // 2b. a SparseTimeValueModel which will be automatically created
|
Chris@670
|
119 // by FeatureExtractionPluginTransformer when running the MATCH
|
Chris@691
|
120 // plugin to perform alignment (so containing the alignment path).
|
Chris@691
|
121 // We call this one pathOutputModel.
|
Chris@672
|
122 //
|
Chris@691
|
123 // 2c. a SparseTimeValueModel used solely to provide faked
|
Chris@691
|
124 // completion information to the AlignmentModel while a
|
Chris@691
|
125 // TuningDifference calculation is going on. We call this
|
Chris@691
|
126 // preparatoryModel.
|
Chris@672
|
127 //
|
Chris@691
|
128 // 3. an AlignmentModel, which stores the path and carries out
|
Chris@691
|
129 // alignment lookups on it. We just call this one alignmentModel.
|
Chris@672
|
130 //
|
Chris@691
|
131 // Models 1 and 3 are registered with the document, which will
|
Chris@691
|
132 // eventually release them. We don't release them here except in
|
Chris@691
|
133 // the case where an activity fails before the point where we
|
Chris@691
|
134 // would otherwise have registered them with the document.
|
Chris@691
|
135 //
|
Chris@691
|
136 // Models 2a (tuningDiffOutputModel), 2b (pathOutputModel) and 2c
|
Chris@691
|
137 // (preparatoryModel) are not registered with the document. Model
|
Chris@691
|
138 // 2b (pathOutputModel) is not registered because we do not have a
|
Chris@691
|
139 // stable reference to the document at the point where it is
|
Chris@691
|
140 // created. Model 2c (preparatoryModel) is not registered because
|
Chris@691
|
141 // it is a bodge that we are embarrassed about, so we try to
|
Chris@691
|
142 // manage it ourselves without anyone else noticing. Model 2a is
|
Chris@691
|
143 // not registered for symmetry with the other two. These have to
|
Chris@691
|
144 // be released by us when finished with, but their lifespans do
|
Chris@691
|
145 // not extend beyond the end of the alignment procedure, so this
|
Chris@691
|
146 // should be ok.
|
Chris@420
|
147
|
Chris@420
|
148 AggregateWaveModel::ChannelSpecList components;
|
Chris@420
|
149
|
Chris@687
|
150 components.push_back
|
Chris@687
|
151 (AggregateWaveModel::ModelChannelSpec(referenceId, -1));
|
Chris@420
|
152
|
Chris@687
|
153 components.push_back
|
Chris@687
|
154 (AggregateWaveModel::ModelChannelSpec(otherId, -1));
|
Chris@420
|
155
|
Chris@683
|
156 auto aggregateModel = std::make_shared<AggregateWaveModel>(components);
|
Chris@687
|
157 auto aggregateModelId = ModelById::add(aggregateModel);
|
Chris@691
|
158 doc->addNonDerivedModel(aggregateModelId);
|
Chris@670
|
159
|
Chris@687
|
160 auto alignmentModel = std::make_shared<AlignmentModel>
|
Chris@687
|
161 (referenceId, otherId, ModelId());
|
Chris@687
|
162 auto alignmentModelId = ModelById::add(alignmentModel);
|
Chris@670
|
163
|
Chris@670
|
164 TransformId tdId = getTuningDifferenceTransformName();
|
Chris@670
|
165
|
Chris@670
|
166 if (tdId == "") {
|
Chris@670
|
167
|
Chris@687
|
168 if (beginTransformDrivenAlignment(aggregateModelId,
|
Chris@687
|
169 alignmentModelId)) {
|
Chris@687
|
170 other->setAlignment(alignmentModelId);
|
Chris@691
|
171 doc->addNonDerivedModel(alignmentModelId);
|
Chris@670
|
172 } else {
|
Chris@670
|
173 error = alignmentModel->getError();
|
Chris@683
|
174 ModelById::release(alignmentModel);
|
Chris@670
|
175 return false;
|
Chris@670
|
176 }
|
Chris@670
|
177
|
Chris@670
|
178 } else {
|
Chris@670
|
179
|
Chris@670
|
180 // Have a tuning-difference transform id, so run it
|
Chris@670
|
181 // asynchronously first
|
Chris@670
|
182
|
Chris@670
|
183 TransformFactory *tf = TransformFactory::getInstance();
|
Chris@670
|
184
|
Chris@670
|
185 Transform transform = tf->getDefaultTransformFor
|
Chris@670
|
186 (tdId, aggregateModel->getSampleRate());
|
Chris@670
|
187
|
Chris@678
|
188 transform.setParameter("maxduration", 60);
|
Chris@678
|
189 transform.setParameter("maxrange", 6);
|
Chris@678
|
190 transform.setParameter("finetuning", false);
|
Chris@671
|
191
|
Chris@670
|
192 SVDEBUG << "Align::alignModel: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
|
Chris@670
|
193
|
Chris@670
|
194 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
|
Chris@670
|
195
|
Chris@670
|
196 QString message;
|
Chris@691
|
197 ModelId tuningDiffOutputModelId = mtf->transform(transform,
|
Chris@691
|
198 aggregateModelId,
|
Chris@691
|
199 message);
|
Chris@670
|
200
|
Chris@691
|
201 auto tuningDiffOutputModel =
|
Chris@691
|
202 ModelById::getAs<SparseTimeValueModel>(tuningDiffOutputModelId);
|
Chris@691
|
203 if (!tuningDiffOutputModel) {
|
Chris@670
|
204 SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
|
Chris@670
|
205 error = message;
|
Chris@691
|
206 ModelById::release(alignmentModel);
|
Chris@670
|
207 return false;
|
Chris@670
|
208 }
|
Chris@670
|
209
|
Chris@687
|
210 other->setAlignment(alignmentModelId);
|
Chris@691
|
211 doc->addNonDerivedModel(alignmentModelId);
|
Chris@665
|
212
|
Chris@691
|
213 connect(tuningDiffOutputModel.get(),
|
Chris@691
|
214 SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
215 this, SLOT(tuningDifferenceCompletionChanged(ModelId)));
|
Chris@420
|
216
|
Chris@671
|
217 TuningDiffRec rec;
|
Chris@687
|
218 rec.input = aggregateModelId;
|
Chris@687
|
219 rec.alignment = alignmentModelId;
|
Chris@677
|
220
|
Chris@671
|
221 // This model exists only so that the AlignmentModel can get a
|
Chris@671
|
222 // completion value from somewhere while the tuning difference
|
Chris@671
|
223 // calculation is going on
|
Chris@683
|
224 auto preparatoryModel = std::make_shared<SparseTimeValueModel>
|
Chris@683
|
225 (aggregateModel->getSampleRate(), 1);
|
Chris@687
|
226 auto preparatoryModelId = ModelById::add(preparatoryModel);
|
Chris@683
|
227 preparatoryModel->setCompletion(0);
|
Chris@687
|
228 rec.preparatory = preparatoryModelId;
|
Chris@671
|
229 alignmentModel->setPathFrom(rec.preparatory);
|
Chris@671
|
230
|
Chris@691
|
231 m_pendingTuningDiffs[tuningDiffOutputModelId] = rec;
|
Chris@670
|
232 }
|
Chris@670
|
233
|
Chris@670
|
234 return true;
|
Chris@670
|
235 }
|
Chris@670
|
236
|
Chris@671
|
237 void
|
Chris@691
|
238 Align::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId)
|
Chris@671
|
239 {
|
Chris@687
|
240 QMutexLocker locker(&m_mutex);
|
Chris@671
|
241
|
Chris@691
|
242 if (m_pendingTuningDiffs.find(tuningDiffOutputModelId) ==
|
Chris@691
|
243 m_pendingTuningDiffs.end()) {
|
Chris@683
|
244 SVCERR << "ERROR: Align::tuningDifferenceCompletionChanged: Model "
|
Chris@691
|
245 << tuningDiffOutputModelId
|
Chris@691
|
246 << " not found in pending tuning diff map!" << endl;
|
Chris@683
|
247 return;
|
Chris@683
|
248 }
|
Chris@671
|
249
|
Chris@691
|
250 auto tuningDiffOutputModel =
|
Chris@691
|
251 ModelById::getAs<SparseTimeValueModel>(tuningDiffOutputModelId);
|
Chris@691
|
252 if (!tuningDiffOutputModel) {
|
Chris@683
|
253 SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged: Model "
|
Chris@691
|
254 << tuningDiffOutputModelId
|
Chris@691
|
255 << " not known as SparseTimeValueModel" << endl;
|
Chris@683
|
256 return;
|
Chris@683
|
257 }
|
Chris@683
|
258
|
Chris@691
|
259 TuningDiffRec rec = m_pendingTuningDiffs[tuningDiffOutputModelId];
|
Chris@683
|
260
|
Chris@691
|
261 auto alignmentModel = ModelById::getAs<AlignmentModel>(rec.alignment);
|
Chris@691
|
262 if (!alignmentModel) {
|
Chris@683
|
263 SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged:"
|
Chris@683
|
264 << "alignment model has disappeared" << endl;
|
Chris@683
|
265 return;
|
Chris@683
|
266 }
|
Chris@683
|
267
|
Chris@671
|
268 int completion = 0;
|
Chris@691
|
269 bool done = tuningDiffOutputModel->isReady(&completion);
|
Chris@671
|
270
|
Chris@671
|
271 if (!done) {
|
Chris@671
|
272 // This will be the completion the alignment model reports,
|
Chris@671
|
273 // before the alignment actually begins. It goes up from 0 to
|
Chris@671
|
274 // 99 (not 100!) and then back to 0 again when we start
|
Chris@671
|
275 // calculating the actual path in the following phase
|
Chris@671
|
276 int clamped = (completion == 100 ? 99 : completion);
|
Chris@691
|
277 auto preparatoryModel =
|
Chris@691
|
278 ModelById::getAs<SparseTimeValueModel>(rec.preparatory);
|
Chris@691
|
279 if (preparatoryModel) {
|
Chris@691
|
280 preparatoryModel->setCompletion(clamped);
|
Chris@683
|
281 }
|
Chris@671
|
282 return;
|
Chris@671
|
283 }
|
Chris@671
|
284
|
Chris@671
|
285 float tuningFrequency = 440.f;
|
Chris@671
|
286
|
Chris@691
|
287 if (!tuningDiffOutputModel->isEmpty()) {
|
Chris@691
|
288 tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue();
|
Chris@671
|
289 SVCERR << "Align::tuningDifferenceCompletionChanged: Reported tuning frequency = " << tuningFrequency << endl;
|
Chris@671
|
290 } else {
|
Chris@671
|
291 SVCERR << "Align::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
|
Chris@671
|
292 }
|
Chris@671
|
293
|
Chris@691
|
294 ModelById::release(tuningDiffOutputModel);
|
Chris@683
|
295
|
Chris@691
|
296 alignmentModel->setPathFrom({}); // replace preparatoryModel
|
Chris@691
|
297 ModelById::release(rec.preparatory);
|
Chris@691
|
298 rec.preparatory = {};
|
Chris@691
|
299
|
Chris@691
|
300 m_pendingTuningDiffs.erase(tuningDiffOutputModelId);
|
Chris@671
|
301
|
Chris@671
|
302 beginTransformDrivenAlignment
|
Chris@671
|
303 (rec.input, rec.alignment, tuningFrequency);
|
Chris@671
|
304 }
|
Chris@671
|
305
|
Chris@670
|
306 bool
|
Chris@683
|
307 Align::beginTransformDrivenAlignment(ModelId aggregateModelId,
|
Chris@683
|
308 ModelId alignmentModelId,
|
Chris@670
|
309 float tuningFrequency)
|
Chris@670
|
310 {
|
Chris@428
|
311 TransformId id = getAlignmentTransformName();
|
Chris@420
|
312
|
Chris@420
|
313 TransformFactory *tf = TransformFactory::getInstance();
|
Chris@420
|
314
|
Chris@691
|
315 auto aggregateModel =
|
Chris@691
|
316 ModelById::getAs<AggregateWaveModel>(aggregateModelId);
|
Chris@691
|
317 auto alignmentModel =
|
Chris@691
|
318 ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@683
|
319
|
Chris@683
|
320 if (!aggregateModel || !alignmentModel) {
|
Chris@683
|
321 SVCERR << "Align::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl;
|
Chris@683
|
322 return false;
|
Chris@683
|
323 }
|
Chris@683
|
324
|
Chris@420
|
325 Transform transform = tf->getDefaultTransformFor
|
Chris@420
|
326 (id, aggregateModel->getSampleRate());
|
Chris@420
|
327
|
Chris@420
|
328 transform.setStepSize(transform.getBlockSize()/2);
|
Chris@420
|
329 transform.setParameter("serialise", 1);
|
Chris@420
|
330 transform.setParameter("smooth", 0);
|
Chris@420
|
331
|
Chris@670
|
332 if (tuningFrequency != 0.f) {
|
Chris@670
|
333 transform.setParameter("freq2", tuningFrequency);
|
Chris@670
|
334 }
|
Chris@670
|
335
|
Chris@420
|
336 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
|
Chris@420
|
337
|
Chris@420
|
338 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
|
Chris@420
|
339
|
Chris@420
|
340 QString message;
|
Chris@691
|
341 ModelId pathOutputModelId = mtf->transform
|
Chris@683
|
342 (transform, aggregateModelId, message);
|
Chris@420
|
343
|
Chris@691
|
344 if (pathOutputModelId.isNone()) {
|
Chris@420
|
345 transform.setStepSize(0);
|
Chris@691
|
346 pathOutputModelId = mtf->transform
|
Chris@683
|
347 (transform, aggregateModelId, message);
|
Chris@420
|
348 }
|
Chris@420
|
349
|
Chris@691
|
350 auto pathOutputModel =
|
Chris@691
|
351 ModelById::getAs<SparseTimeValueModel>(pathOutputModelId);
|
Chris@420
|
352
|
Chris@670
|
353 //!!! callers will need to be updated to get error from
|
Chris@670
|
354 //!!! alignment model after initial call
|
Chris@670
|
355
|
Chris@691
|
356 if (!pathOutputModel) {
|
Chris@649
|
357 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
|
Chris@670
|
358 alignmentModel->setError(message);
|
Chris@420
|
359 return false;
|
Chris@420
|
360 }
|
Chris@420
|
361
|
Chris@691
|
362 pathOutputModel->setCompletion(0);
|
Chris@691
|
363 alignmentModel->setPathFrom(pathOutputModelId);
|
Chris@691
|
364
|
Chris@691
|
365 m_pendingAlignments[alignmentModelId] = pathOutputModelId;
|
Chris@420
|
366
|
Chris@687
|
367 connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
368 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@420
|
369
|
Chris@420
|
370 return true;
|
Chris@420
|
371 }
|
Chris@420
|
372
|
Chris@428
|
373 void
|
Chris@691
|
374 Align::alignmentCompletionChanged(ModelId alignmentModelId)
|
Chris@428
|
375 {
|
Chris@670
|
376 QMutexLocker locker (&m_mutex);
|
Chris@683
|
377
|
Chris@691
|
378 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@691
|
379
|
Chris@691
|
380 if (alignmentModel && alignmentModel->isReady()) {
|
Chris@691
|
381
|
Chris@691
|
382 if (m_pendingAlignments.find(alignmentModelId) !=
|
Chris@691
|
383 m_pendingAlignments.end()) {
|
Chris@691
|
384 ModelId pathOutputModelId = m_pendingAlignments[alignmentModelId];
|
Chris@691
|
385 ModelById::release(pathOutputModelId);
|
Chris@691
|
386 m_pendingAlignments.erase(alignmentModelId);
|
Chris@691
|
387 }
|
Chris@691
|
388
|
Chris@691
|
389 disconnect(alignmentModel.get(),
|
Chris@691
|
390 SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
391 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@691
|
392 emit alignmentComplete(alignmentModelId);
|
Chris@428
|
393 }
|
Chris@428
|
394 }
|
Chris@428
|
395
|
Chris@420
|
396 bool
|
Chris@691
|
397 Align::alignModelViaProgram(Document *doc,
|
Chris@687
|
398 ModelId referenceId,
|
Chris@687
|
399 ModelId otherId,
|
Chris@687
|
400 QString program,
|
Chris@687
|
401 QString &error)
|
Chris@420
|
402 {
|
Chris@670
|
403 QMutexLocker locker (&m_mutex);
|
Chris@430
|
404
|
Chris@420
|
405 // Run an external program, passing to it paths to the main
|
Chris@420
|
406 // model's audio file and the new model's audio file. It returns
|
Chris@420
|
407 // the path in CSV form through stdout.
|
Chris@420
|
408
|
Chris@687
|
409 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(referenceId);
|
Chris@687
|
410 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(otherId);
|
Chris@687
|
411 if (!reference || !other) {
|
Chris@649
|
412 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
|
Chris@515
|
413 return false;
|
Chris@515
|
414 }
|
Chris@687
|
415
|
Chris@687
|
416 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
|
Chris@687
|
417 qApp->processEvents();
|
Chris@687
|
418 }
|
Chris@515
|
419
|
Chris@687
|
420 QString refPath = reference->getLocalFilename();
|
Chris@687
|
421 QString otherPath = other->getLocalFilename();
|
Chris@420
|
422
|
Chris@420
|
423 if (refPath == "" || otherPath == "") {
|
Chris@670
|
424 error = "Failed to find local filepath for wave-file model";
|
Chris@595
|
425 return false;
|
Chris@420
|
426 }
|
Chris@420
|
427
|
Chris@687
|
428 auto alignmentModel =
|
Chris@687
|
429 std::make_shared<AlignmentModel>(referenceId, otherId, ModelId());
|
Chris@687
|
430 auto alignmentModelId = ModelById::add(alignmentModel);
|
Chris@687
|
431 other->setAlignment(alignmentModelId);
|
Chris@423
|
432
|
Chris@423
|
433 QProcess *process = new QProcess;
|
Chris@420
|
434 QStringList args;
|
Chris@420
|
435 args << refPath << otherPath;
|
Chris@423
|
436
|
Chris@423
|
437 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
|
Chris@423
|
438 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
|
Chris@420
|
439
|
Chris@687
|
440 m_pendingProcesses[process] = alignmentModelId;
|
Chris@423
|
441 process->start(program, args);
|
Chris@420
|
442
|
Chris@423
|
443 bool success = process->waitForStarted();
|
Chris@423
|
444
|
Chris@423
|
445 if (!success) {
|
Chris@649
|
446 SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
|
Chris@649
|
447 << endl;
|
Chris@670
|
448 error = "Alignment program could not be started";
|
Chris@670
|
449 m_pendingProcesses.erase(process);
|
Chris@687
|
450 other->setAlignment({});
|
Chris@691
|
451 ModelById::release(alignmentModelId);
|
Chris@423
|
452 delete process;
|
Chris@423
|
453 }
|
Chris@423
|
454
|
Chris@691
|
455 doc->addNonDerivedModel(alignmentModelId);
|
Chris@423
|
456 return success;
|
Chris@423
|
457 }
|
Chris@423
|
458
|
Chris@423
|
459 void
|
Chris@423
|
460 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
|
Chris@423
|
461 {
|
Chris@670
|
462 QMutexLocker locker (&m_mutex);
|
Chris@670
|
463
|
Chris@649
|
464 SVCERR << "Align::alignmentProgramFinished" << endl;
|
Chris@423
|
465
|
Chris@423
|
466 QProcess *process = qobject_cast<QProcess *>(sender());
|
Chris@423
|
467
|
Chris@670
|
468 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
|
Chris@649
|
469 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
|
Chris@649
|
470 << " not found in process model map!" << endl;
|
Chris@423
|
471 return;
|
Chris@423
|
472 }
|
Chris@423
|
473
|
Chris@683
|
474 ModelId alignmentModelId = m_pendingProcesses[process];
|
Chris@683
|
475 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@683
|
476 if (!alignmentModel) return;
|
Chris@423
|
477
|
Chris@423
|
478 if (exitCode == 0 && status == 0) {
|
Chris@420
|
479
|
Chris@595
|
480 CSVFormat format;
|
Chris@595
|
481 format.setModelType(CSVFormat::TwoDimensionalModel);
|
Chris@595
|
482 format.setTimingType(CSVFormat::ExplicitTiming);
|
Chris@595
|
483 format.setTimeUnits(CSVFormat::TimeSeconds);
|
Chris@595
|
484 format.setColumnCount(2);
|
Chris@425
|
485 // The output format has time in the reference file first, and
|
Chris@425
|
486 // time in the "other" file in the second column. This is a
|
Chris@425
|
487 // more natural approach for a command-line alignment tool,
|
Chris@425
|
488 // but it's the opposite of what we expect for native
|
Chris@425
|
489 // alignment paths, which map from "other" file to
|
Chris@425
|
490 // reference. These column purpose settings reflect that.
|
Chris@595
|
491 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
|
Chris@595
|
492 format.setColumnPurpose(0, CSVFormat::ColumnValue);
|
Chris@595
|
493 format.setAllowQuoting(false);
|
Chris@595
|
494 format.setSeparator(',');
|
Chris@420
|
495
|
Chris@595
|
496 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
|
Chris@595
|
497 if (!reader.isOK()) {
|
Chris@649
|
498 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
|
Chris@649
|
499 << endl;
|
Chris@670
|
500 alignmentModel->setError
|
Chris@670
|
501 (QString("Failed to parse output of program: %1")
|
Chris@670
|
502 .arg(reader.getError()));
|
Chris@423
|
503 goto done;
|
Chris@595
|
504 }
|
Chris@420
|
505
|
Chris@683
|
506 //!!! to use ById?
|
Chris@683
|
507
|
Chris@595
|
508 Model *csvOutput = reader.load();
|
Chris@420
|
509
|
Chris@687
|
510 SparseTimeValueModel *path =
|
Chris@687
|
511 qobject_cast<SparseTimeValueModel *>(csvOutput);
|
Chris@595
|
512 if (!path) {
|
Chris@649
|
513 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
|
Chris@649
|
514 << endl;
|
Chris@670
|
515 alignmentModel->setError
|
Chris@670
|
516 ("Output of program did not produce sparse time-value model");
|
Chris@683
|
517 delete csvOutput;
|
Chris@423
|
518 goto done;
|
Chris@595
|
519 }
|
Chris@683
|
520
|
Chris@649
|
521 if (path->isEmpty()) {
|
Chris@649
|
522 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
|
Chris@649
|
523 << endl;
|
Chris@670
|
524 alignmentModel->setError
|
Chris@670
|
525 ("Output of alignment program contained no mappings");
|
Chris@683
|
526 delete path;
|
Chris@423
|
527 goto done;
|
Chris@595
|
528 }
|
Chris@420
|
529
|
Chris@649
|
530 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
|
Chris@650
|
531 << path->getEventCount() << " point(s))" << endl;
|
Chris@650
|
532
|
Chris@687
|
533 auto pathId =
|
Chris@687
|
534 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
|
Chris@687
|
535 alignmentModel->setPathFrom(pathId);
|
Chris@420
|
536
|
Chris@683
|
537 emit alignmentComplete(alignmentModelId);
|
Chris@687
|
538
|
Chris@687
|
539 ModelById::release(pathId);
|
Chris@428
|
540
|
Chris@420
|
541 } else {
|
Chris@649
|
542 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
|
Chris@649
|
543 << "failed: exit code " << exitCode << ", status " << status
|
Chris@649
|
544 << endl;
|
Chris@670
|
545 alignmentModel->setError
|
Chris@670
|
546 ("Aligner process returned non-zero exit status");
|
Chris@420
|
547 }
|
Chris@420
|
548
|
Chris@423
|
549 done:
|
Chris@670
|
550 m_pendingProcesses.erase(process);
|
Chris@423
|
551 delete process;
|
Chris@420
|
552 }
|
Chris@420
|
553
|