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@698
|
232
|
Chris@698
|
233 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
|
234 }
|
Chris@670
|
235
|
Chris@670
|
236 return true;
|
Chris@670
|
237 }
|
Chris@670
|
238
|
Chris@671
|
239 void
|
Chris@691
|
240 Align::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId)
|
Chris@671
|
241 {
|
Chris@687
|
242 QMutexLocker locker(&m_mutex);
|
Chris@671
|
243
|
Chris@691
|
244 if (m_pendingTuningDiffs.find(tuningDiffOutputModelId) ==
|
Chris@691
|
245 m_pendingTuningDiffs.end()) {
|
Chris@698
|
246 SVDEBUG << "NOTE: Align::tuningDifferenceCompletionChanged: Model "
|
Chris@698
|
247 << tuningDiffOutputModelId
|
Chris@698
|
248 << " not found in pending tuning diff map, probably "
|
Chris@698
|
249 << "completed already" << endl;
|
Chris@683
|
250 return;
|
Chris@683
|
251 }
|
Chris@671
|
252
|
Chris@691
|
253 auto tuningDiffOutputModel =
|
Chris@691
|
254 ModelById::getAs<SparseTimeValueModel>(tuningDiffOutputModelId);
|
Chris@691
|
255 if (!tuningDiffOutputModel) {
|
Chris@683
|
256 SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged: Model "
|
Chris@691
|
257 << tuningDiffOutputModelId
|
Chris@691
|
258 << " not known as SparseTimeValueModel" << endl;
|
Chris@683
|
259 return;
|
Chris@683
|
260 }
|
Chris@683
|
261
|
Chris@691
|
262 TuningDiffRec rec = m_pendingTuningDiffs[tuningDiffOutputModelId];
|
Chris@683
|
263
|
Chris@691
|
264 auto alignmentModel = ModelById::getAs<AlignmentModel>(rec.alignment);
|
Chris@691
|
265 if (!alignmentModel) {
|
Chris@683
|
266 SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged:"
|
Chris@683
|
267 << "alignment model has disappeared" << endl;
|
Chris@683
|
268 return;
|
Chris@683
|
269 }
|
Chris@683
|
270
|
Chris@671
|
271 int completion = 0;
|
Chris@691
|
272 bool done = tuningDiffOutputModel->isReady(&completion);
|
Chris@671
|
273
|
Chris@671
|
274 if (!done) {
|
Chris@671
|
275 // This will be the completion the alignment model reports,
|
Chris@671
|
276 // before the alignment actually begins. It goes up from 0 to
|
Chris@671
|
277 // 99 (not 100!) and then back to 0 again when we start
|
Chris@671
|
278 // calculating the actual path in the following phase
|
Chris@671
|
279 int clamped = (completion == 100 ? 99 : completion);
|
Chris@691
|
280 auto preparatoryModel =
|
Chris@691
|
281 ModelById::getAs<SparseTimeValueModel>(rec.preparatory);
|
Chris@691
|
282 if (preparatoryModel) {
|
Chris@691
|
283 preparatoryModel->setCompletion(clamped);
|
Chris@683
|
284 }
|
Chris@671
|
285 return;
|
Chris@671
|
286 }
|
Chris@671
|
287
|
Chris@671
|
288 float tuningFrequency = 440.f;
|
Chris@671
|
289
|
Chris@691
|
290 if (!tuningDiffOutputModel->isEmpty()) {
|
Chris@691
|
291 tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue();
|
Chris@671
|
292 SVCERR << "Align::tuningDifferenceCompletionChanged: Reported tuning frequency = " << tuningFrequency << endl;
|
Chris@671
|
293 } else {
|
Chris@671
|
294 SVCERR << "Align::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
|
Chris@671
|
295 }
|
Chris@698
|
296
|
Chris@691
|
297 ModelById::release(tuningDiffOutputModel);
|
Chris@683
|
298
|
Chris@691
|
299 alignmentModel->setPathFrom({}); // replace preparatoryModel
|
Chris@691
|
300 ModelById::release(rec.preparatory);
|
Chris@691
|
301 rec.preparatory = {};
|
Chris@691
|
302
|
Chris@691
|
303 m_pendingTuningDiffs.erase(tuningDiffOutputModelId);
|
Chris@698
|
304
|
Chris@698
|
305 SVDEBUG << "Align::tuningDifferenceCompletionChanged: Erasing model "
|
Chris@698
|
306 << tuningDiffOutputModelId << " from pending tuning diffs and "
|
Chris@698
|
307 << "launching the alignment phase for alignment model "
|
Chris@698
|
308 << rec.alignment << " with tuning frequency "
|
Chris@698
|
309 << tuningFrequency << endl;
|
Chris@671
|
310
|
Chris@671
|
311 beginTransformDrivenAlignment
|
Chris@671
|
312 (rec.input, rec.alignment, tuningFrequency);
|
Chris@671
|
313 }
|
Chris@671
|
314
|
Chris@670
|
315 bool
|
Chris@683
|
316 Align::beginTransformDrivenAlignment(ModelId aggregateModelId,
|
Chris@683
|
317 ModelId alignmentModelId,
|
Chris@670
|
318 float tuningFrequency)
|
Chris@670
|
319 {
|
Chris@428
|
320 TransformId id = getAlignmentTransformName();
|
Chris@420
|
321
|
Chris@420
|
322 TransformFactory *tf = TransformFactory::getInstance();
|
Chris@420
|
323
|
Chris@691
|
324 auto aggregateModel =
|
Chris@691
|
325 ModelById::getAs<AggregateWaveModel>(aggregateModelId);
|
Chris@691
|
326 auto alignmentModel =
|
Chris@691
|
327 ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@683
|
328
|
Chris@683
|
329 if (!aggregateModel || !alignmentModel) {
|
Chris@683
|
330 SVCERR << "Align::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl;
|
Chris@683
|
331 return false;
|
Chris@683
|
332 }
|
Chris@683
|
333
|
Chris@420
|
334 Transform transform = tf->getDefaultTransformFor
|
Chris@420
|
335 (id, aggregateModel->getSampleRate());
|
Chris@420
|
336
|
Chris@420
|
337 transform.setStepSize(transform.getBlockSize()/2);
|
Chris@420
|
338 transform.setParameter("serialise", 1);
|
Chris@420
|
339 transform.setParameter("smooth", 0);
|
Chris@420
|
340
|
Chris@670
|
341 if (tuningFrequency != 0.f) {
|
Chris@670
|
342 transform.setParameter("freq2", tuningFrequency);
|
Chris@670
|
343 }
|
Chris@670
|
344
|
Chris@420
|
345 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
|
Chris@420
|
346
|
Chris@420
|
347 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
|
Chris@420
|
348
|
Chris@420
|
349 QString message;
|
Chris@691
|
350 ModelId pathOutputModelId = mtf->transform
|
Chris@683
|
351 (transform, aggregateModelId, message);
|
Chris@420
|
352
|
Chris@691
|
353 if (pathOutputModelId.isNone()) {
|
Chris@420
|
354 transform.setStepSize(0);
|
Chris@691
|
355 pathOutputModelId = mtf->transform
|
Chris@683
|
356 (transform, aggregateModelId, message);
|
Chris@420
|
357 }
|
Chris@420
|
358
|
Chris@691
|
359 auto pathOutputModel =
|
Chris@691
|
360 ModelById::getAs<SparseTimeValueModel>(pathOutputModelId);
|
Chris@420
|
361
|
Chris@670
|
362 //!!! callers will need to be updated to get error from
|
Chris@670
|
363 //!!! alignment model after initial call
|
Chris@670
|
364
|
Chris@691
|
365 if (!pathOutputModel) {
|
Chris@649
|
366 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
|
Chris@670
|
367 alignmentModel->setError(message);
|
Chris@420
|
368 return false;
|
Chris@420
|
369 }
|
Chris@420
|
370
|
Chris@691
|
371 pathOutputModel->setCompletion(0);
|
Chris@691
|
372 alignmentModel->setPathFrom(pathOutputModelId);
|
Chris@691
|
373
|
Chris@691
|
374 m_pendingAlignments[alignmentModelId] = pathOutputModelId;
|
Chris@420
|
375
|
Chris@687
|
376 connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
377 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@420
|
378
|
Chris@420
|
379 return true;
|
Chris@420
|
380 }
|
Chris@420
|
381
|
Chris@428
|
382 void
|
Chris@691
|
383 Align::alignmentCompletionChanged(ModelId alignmentModelId)
|
Chris@428
|
384 {
|
Chris@670
|
385 QMutexLocker locker (&m_mutex);
|
Chris@683
|
386
|
Chris@691
|
387 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@691
|
388
|
Chris@691
|
389 if (alignmentModel && alignmentModel->isReady()) {
|
Chris@691
|
390
|
Chris@691
|
391 if (m_pendingAlignments.find(alignmentModelId) !=
|
Chris@691
|
392 m_pendingAlignments.end()) {
|
Chris@691
|
393 ModelId pathOutputModelId = m_pendingAlignments[alignmentModelId];
|
Chris@691
|
394 ModelById::release(pathOutputModelId);
|
Chris@691
|
395 m_pendingAlignments.erase(alignmentModelId);
|
Chris@691
|
396 }
|
Chris@691
|
397
|
Chris@691
|
398 disconnect(alignmentModel.get(),
|
Chris@691
|
399 SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
400 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@691
|
401 emit alignmentComplete(alignmentModelId);
|
Chris@428
|
402 }
|
Chris@428
|
403 }
|
Chris@428
|
404
|
Chris@420
|
405 bool
|
Chris@691
|
406 Align::alignModelViaProgram(Document *doc,
|
Chris@687
|
407 ModelId referenceId,
|
Chris@687
|
408 ModelId otherId,
|
Chris@687
|
409 QString program,
|
Chris@687
|
410 QString &error)
|
Chris@420
|
411 {
|
Chris@670
|
412 QMutexLocker locker (&m_mutex);
|
Chris@430
|
413
|
Chris@420
|
414 // Run an external program, passing to it paths to the main
|
Chris@420
|
415 // model's audio file and the new model's audio file. It returns
|
Chris@420
|
416 // the path in CSV form through stdout.
|
Chris@420
|
417
|
Chris@687
|
418 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(referenceId);
|
Chris@687
|
419 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(otherId);
|
Chris@687
|
420 if (!reference || !other) {
|
Chris@649
|
421 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
|
Chris@515
|
422 return false;
|
Chris@515
|
423 }
|
Chris@687
|
424
|
Chris@687
|
425 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
|
Chris@687
|
426 qApp->processEvents();
|
Chris@687
|
427 }
|
Chris@515
|
428
|
Chris@687
|
429 QString refPath = reference->getLocalFilename();
|
Chris@687
|
430 QString otherPath = other->getLocalFilename();
|
Chris@420
|
431
|
Chris@420
|
432 if (refPath == "" || otherPath == "") {
|
Chris@670
|
433 error = "Failed to find local filepath for wave-file model";
|
Chris@595
|
434 return false;
|
Chris@420
|
435 }
|
Chris@420
|
436
|
Chris@687
|
437 auto alignmentModel =
|
Chris@687
|
438 std::make_shared<AlignmentModel>(referenceId, otherId, ModelId());
|
Chris@687
|
439 auto alignmentModelId = ModelById::add(alignmentModel);
|
Chris@687
|
440 other->setAlignment(alignmentModelId);
|
Chris@423
|
441
|
Chris@423
|
442 QProcess *process = new QProcess;
|
Chris@420
|
443 QStringList args;
|
Chris@420
|
444 args << refPath << otherPath;
|
Chris@423
|
445
|
Chris@423
|
446 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
|
Chris@423
|
447 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
|
Chris@420
|
448
|
Chris@687
|
449 m_pendingProcesses[process] = alignmentModelId;
|
Chris@423
|
450 process->start(program, args);
|
Chris@420
|
451
|
Chris@423
|
452 bool success = process->waitForStarted();
|
Chris@423
|
453
|
Chris@423
|
454 if (!success) {
|
Chris@649
|
455 SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
|
Chris@649
|
456 << endl;
|
Chris@670
|
457 error = "Alignment program could not be started";
|
Chris@670
|
458 m_pendingProcesses.erase(process);
|
Chris@687
|
459 other->setAlignment({});
|
Chris@691
|
460 ModelById::release(alignmentModelId);
|
Chris@423
|
461 delete process;
|
Chris@423
|
462 }
|
Chris@423
|
463
|
Chris@691
|
464 doc->addNonDerivedModel(alignmentModelId);
|
Chris@423
|
465 return success;
|
Chris@423
|
466 }
|
Chris@423
|
467
|
Chris@423
|
468 void
|
Chris@423
|
469 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
|
Chris@423
|
470 {
|
Chris@670
|
471 QMutexLocker locker (&m_mutex);
|
Chris@670
|
472
|
Chris@649
|
473 SVCERR << "Align::alignmentProgramFinished" << endl;
|
Chris@423
|
474
|
Chris@423
|
475 QProcess *process = qobject_cast<QProcess *>(sender());
|
Chris@423
|
476
|
Chris@670
|
477 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
|
Chris@649
|
478 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
|
Chris@649
|
479 << " not found in process model map!" << endl;
|
Chris@423
|
480 return;
|
Chris@423
|
481 }
|
Chris@423
|
482
|
Chris@683
|
483 ModelId alignmentModelId = m_pendingProcesses[process];
|
Chris@683
|
484 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@683
|
485 if (!alignmentModel) return;
|
Chris@423
|
486
|
Chris@423
|
487 if (exitCode == 0 && status == 0) {
|
Chris@420
|
488
|
Chris@595
|
489 CSVFormat format;
|
Chris@595
|
490 format.setModelType(CSVFormat::TwoDimensionalModel);
|
Chris@595
|
491 format.setTimingType(CSVFormat::ExplicitTiming);
|
Chris@595
|
492 format.setTimeUnits(CSVFormat::TimeSeconds);
|
Chris@595
|
493 format.setColumnCount(2);
|
Chris@425
|
494 // The output format has time in the reference file first, and
|
Chris@425
|
495 // time in the "other" file in the second column. This is a
|
Chris@425
|
496 // more natural approach for a command-line alignment tool,
|
Chris@425
|
497 // but it's the opposite of what we expect for native
|
Chris@425
|
498 // alignment paths, which map from "other" file to
|
Chris@425
|
499 // reference. These column purpose settings reflect that.
|
Chris@595
|
500 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
|
Chris@595
|
501 format.setColumnPurpose(0, CSVFormat::ColumnValue);
|
Chris@595
|
502 format.setAllowQuoting(false);
|
Chris@595
|
503 format.setSeparator(',');
|
Chris@420
|
504
|
Chris@595
|
505 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
|
Chris@595
|
506 if (!reader.isOK()) {
|
Chris@649
|
507 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
|
Chris@649
|
508 << endl;
|
Chris@670
|
509 alignmentModel->setError
|
Chris@670
|
510 (QString("Failed to parse output of program: %1")
|
Chris@670
|
511 .arg(reader.getError()));
|
Chris@423
|
512 goto done;
|
Chris@595
|
513 }
|
Chris@420
|
514
|
Chris@683
|
515 //!!! to use ById?
|
Chris@683
|
516
|
Chris@595
|
517 Model *csvOutput = reader.load();
|
Chris@420
|
518
|
Chris@687
|
519 SparseTimeValueModel *path =
|
Chris@687
|
520 qobject_cast<SparseTimeValueModel *>(csvOutput);
|
Chris@595
|
521 if (!path) {
|
Chris@649
|
522 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
|
Chris@649
|
523 << endl;
|
Chris@670
|
524 alignmentModel->setError
|
Chris@670
|
525 ("Output of program did not produce sparse time-value model");
|
Chris@683
|
526 delete csvOutput;
|
Chris@423
|
527 goto done;
|
Chris@595
|
528 }
|
Chris@683
|
529
|
Chris@649
|
530 if (path->isEmpty()) {
|
Chris@649
|
531 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
|
Chris@649
|
532 << endl;
|
Chris@670
|
533 alignmentModel->setError
|
Chris@670
|
534 ("Output of alignment program contained no mappings");
|
Chris@683
|
535 delete path;
|
Chris@423
|
536 goto done;
|
Chris@595
|
537 }
|
Chris@420
|
538
|
Chris@649
|
539 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
|
Chris@650
|
540 << path->getEventCount() << " point(s))" << endl;
|
Chris@650
|
541
|
Chris@687
|
542 auto pathId =
|
Chris@687
|
543 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
|
Chris@687
|
544 alignmentModel->setPathFrom(pathId);
|
Chris@420
|
545
|
Chris@683
|
546 emit alignmentComplete(alignmentModelId);
|
Chris@687
|
547
|
Chris@687
|
548 ModelById::release(pathId);
|
Chris@428
|
549
|
Chris@420
|
550 } else {
|
Chris@649
|
551 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
|
Chris@649
|
552 << "failed: exit code " << exitCode << ", status " << status
|
Chris@649
|
553 << endl;
|
Chris@670
|
554 alignmentModel->setError
|
Chris@670
|
555 ("Aligner process returned non-zero exit status");
|
Chris@420
|
556 }
|
Chris@420
|
557
|
Chris@423
|
558 done:
|
Chris@670
|
559 m_pendingProcesses.erase(process);
|
Chris@423
|
560 delete process;
|
Chris@420
|
561 }
|
Chris@420
|
562
|