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@699
|
340 transform.setParameter("zonewidth", 40);
|
Chris@701
|
341 // transform.setParameter("noise", true);
|
Chris@699
|
342 transform.setParameter("minfreq", 250);
|
Chris@701
|
343 // transform.setParameter("usechroma", 1);
|
Chris@420
|
344
|
Chris@670
|
345 if (tuningFrequency != 0.f) {
|
Chris@670
|
346 transform.setParameter("freq2", tuningFrequency);
|
Chris@670
|
347 }
|
Chris@670
|
348
|
Chris@420
|
349 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
|
Chris@420
|
350
|
Chris@420
|
351 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
|
Chris@420
|
352
|
Chris@420
|
353 QString message;
|
Chris@691
|
354 ModelId pathOutputModelId = mtf->transform
|
Chris@683
|
355 (transform, aggregateModelId, message);
|
Chris@420
|
356
|
Chris@691
|
357 if (pathOutputModelId.isNone()) {
|
Chris@420
|
358 transform.setStepSize(0);
|
Chris@691
|
359 pathOutputModelId = mtf->transform
|
Chris@683
|
360 (transform, aggregateModelId, message);
|
Chris@420
|
361 }
|
Chris@420
|
362
|
Chris@691
|
363 auto pathOutputModel =
|
Chris@691
|
364 ModelById::getAs<SparseTimeValueModel>(pathOutputModelId);
|
Chris@420
|
365
|
Chris@670
|
366 //!!! callers will need to be updated to get error from
|
Chris@670
|
367 //!!! alignment model after initial call
|
Chris@670
|
368
|
Chris@691
|
369 if (!pathOutputModel) {
|
Chris@649
|
370 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
|
Chris@670
|
371 alignmentModel->setError(message);
|
Chris@420
|
372 return false;
|
Chris@420
|
373 }
|
Chris@420
|
374
|
Chris@691
|
375 pathOutputModel->setCompletion(0);
|
Chris@691
|
376 alignmentModel->setPathFrom(pathOutputModelId);
|
Chris@691
|
377
|
Chris@691
|
378 m_pendingAlignments[alignmentModelId] = pathOutputModelId;
|
Chris@420
|
379
|
Chris@687
|
380 connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
381 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@420
|
382
|
Chris@420
|
383 return true;
|
Chris@420
|
384 }
|
Chris@420
|
385
|
Chris@428
|
386 void
|
Chris@691
|
387 Align::alignmentCompletionChanged(ModelId alignmentModelId)
|
Chris@428
|
388 {
|
Chris@670
|
389 QMutexLocker locker (&m_mutex);
|
Chris@683
|
390
|
Chris@691
|
391 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@691
|
392
|
Chris@691
|
393 if (alignmentModel && alignmentModel->isReady()) {
|
Chris@691
|
394
|
Chris@691
|
395 if (m_pendingAlignments.find(alignmentModelId) !=
|
Chris@691
|
396 m_pendingAlignments.end()) {
|
Chris@691
|
397 ModelId pathOutputModelId = m_pendingAlignments[alignmentModelId];
|
Chris@691
|
398 ModelById::release(pathOutputModelId);
|
Chris@691
|
399 m_pendingAlignments.erase(alignmentModelId);
|
Chris@691
|
400 }
|
Chris@691
|
401
|
Chris@691
|
402 disconnect(alignmentModel.get(),
|
Chris@691
|
403 SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
404 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@691
|
405 emit alignmentComplete(alignmentModelId);
|
Chris@428
|
406 }
|
Chris@428
|
407 }
|
Chris@428
|
408
|
Chris@420
|
409 bool
|
Chris@691
|
410 Align::alignModelViaProgram(Document *doc,
|
Chris@687
|
411 ModelId referenceId,
|
Chris@687
|
412 ModelId otherId,
|
Chris@687
|
413 QString program,
|
Chris@687
|
414 QString &error)
|
Chris@420
|
415 {
|
Chris@670
|
416 QMutexLocker locker (&m_mutex);
|
Chris@430
|
417
|
Chris@420
|
418 // Run an external program, passing to it paths to the main
|
Chris@420
|
419 // model's audio file and the new model's audio file. It returns
|
Chris@420
|
420 // the path in CSV form through stdout.
|
Chris@420
|
421
|
Chris@687
|
422 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(referenceId);
|
Chris@687
|
423 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(otherId);
|
Chris@687
|
424 if (!reference || !other) {
|
Chris@649
|
425 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
|
Chris@515
|
426 return false;
|
Chris@515
|
427 }
|
Chris@687
|
428
|
Chris@687
|
429 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
|
Chris@687
|
430 qApp->processEvents();
|
Chris@687
|
431 }
|
Chris@515
|
432
|
Chris@687
|
433 QString refPath = reference->getLocalFilename();
|
Chris@687
|
434 QString otherPath = other->getLocalFilename();
|
Chris@420
|
435
|
Chris@420
|
436 if (refPath == "" || otherPath == "") {
|
Chris@670
|
437 error = "Failed to find local filepath for wave-file model";
|
Chris@595
|
438 return false;
|
Chris@420
|
439 }
|
Chris@420
|
440
|
Chris@687
|
441 auto alignmentModel =
|
Chris@687
|
442 std::make_shared<AlignmentModel>(referenceId, otherId, ModelId());
|
Chris@687
|
443 auto alignmentModelId = ModelById::add(alignmentModel);
|
Chris@687
|
444 other->setAlignment(alignmentModelId);
|
Chris@423
|
445
|
Chris@423
|
446 QProcess *process = new QProcess;
|
Chris@420
|
447 QStringList args;
|
Chris@420
|
448 args << refPath << otherPath;
|
Chris@423
|
449
|
Chris@423
|
450 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
|
Chris@423
|
451 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
|
Chris@420
|
452
|
Chris@687
|
453 m_pendingProcesses[process] = alignmentModelId;
|
Chris@423
|
454 process->start(program, args);
|
Chris@420
|
455
|
Chris@423
|
456 bool success = process->waitForStarted();
|
Chris@423
|
457
|
Chris@423
|
458 if (!success) {
|
Chris@649
|
459 SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
|
Chris@649
|
460 << endl;
|
Chris@670
|
461 error = "Alignment program could not be started";
|
Chris@670
|
462 m_pendingProcesses.erase(process);
|
Chris@687
|
463 other->setAlignment({});
|
Chris@691
|
464 ModelById::release(alignmentModelId);
|
Chris@423
|
465 delete process;
|
Chris@423
|
466 }
|
Chris@423
|
467
|
Chris@691
|
468 doc->addNonDerivedModel(alignmentModelId);
|
Chris@423
|
469 return success;
|
Chris@423
|
470 }
|
Chris@423
|
471
|
Chris@423
|
472 void
|
Chris@423
|
473 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
|
Chris@423
|
474 {
|
Chris@670
|
475 QMutexLocker locker (&m_mutex);
|
Chris@670
|
476
|
Chris@649
|
477 SVCERR << "Align::alignmentProgramFinished" << endl;
|
Chris@423
|
478
|
Chris@423
|
479 QProcess *process = qobject_cast<QProcess *>(sender());
|
Chris@423
|
480
|
Chris@670
|
481 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
|
Chris@649
|
482 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
|
Chris@649
|
483 << " not found in process model map!" << endl;
|
Chris@423
|
484 return;
|
Chris@423
|
485 }
|
Chris@423
|
486
|
Chris@683
|
487 ModelId alignmentModelId = m_pendingProcesses[process];
|
Chris@683
|
488 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@683
|
489 if (!alignmentModel) return;
|
Chris@423
|
490
|
Chris@423
|
491 if (exitCode == 0 && status == 0) {
|
Chris@420
|
492
|
Chris@595
|
493 CSVFormat format;
|
Chris@595
|
494 format.setModelType(CSVFormat::TwoDimensionalModel);
|
Chris@595
|
495 format.setTimingType(CSVFormat::ExplicitTiming);
|
Chris@595
|
496 format.setTimeUnits(CSVFormat::TimeSeconds);
|
Chris@595
|
497 format.setColumnCount(2);
|
Chris@425
|
498 // The output format has time in the reference file first, and
|
Chris@425
|
499 // time in the "other" file in the second column. This is a
|
Chris@425
|
500 // more natural approach for a command-line alignment tool,
|
Chris@425
|
501 // but it's the opposite of what we expect for native
|
Chris@425
|
502 // alignment paths, which map from "other" file to
|
Chris@425
|
503 // reference. These column purpose settings reflect that.
|
Chris@595
|
504 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
|
Chris@595
|
505 format.setColumnPurpose(0, CSVFormat::ColumnValue);
|
Chris@595
|
506 format.setAllowQuoting(false);
|
Chris@595
|
507 format.setSeparator(',');
|
Chris@420
|
508
|
Chris@595
|
509 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
|
Chris@595
|
510 if (!reader.isOK()) {
|
Chris@649
|
511 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
|
Chris@649
|
512 << endl;
|
Chris@670
|
513 alignmentModel->setError
|
Chris@670
|
514 (QString("Failed to parse output of program: %1")
|
Chris@670
|
515 .arg(reader.getError()));
|
Chris@423
|
516 goto done;
|
Chris@595
|
517 }
|
Chris@420
|
518
|
Chris@683
|
519 //!!! to use ById?
|
Chris@683
|
520
|
Chris@595
|
521 Model *csvOutput = reader.load();
|
Chris@420
|
522
|
Chris@687
|
523 SparseTimeValueModel *path =
|
Chris@687
|
524 qobject_cast<SparseTimeValueModel *>(csvOutput);
|
Chris@595
|
525 if (!path) {
|
Chris@649
|
526 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
|
Chris@649
|
527 << endl;
|
Chris@670
|
528 alignmentModel->setError
|
Chris@670
|
529 ("Output of program did not produce sparse time-value model");
|
Chris@683
|
530 delete csvOutput;
|
Chris@423
|
531 goto done;
|
Chris@595
|
532 }
|
Chris@683
|
533
|
Chris@649
|
534 if (path->isEmpty()) {
|
Chris@649
|
535 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
|
Chris@649
|
536 << endl;
|
Chris@670
|
537 alignmentModel->setError
|
Chris@670
|
538 ("Output of alignment program contained no mappings");
|
Chris@683
|
539 delete path;
|
Chris@423
|
540 goto done;
|
Chris@595
|
541 }
|
Chris@420
|
542
|
Chris@649
|
543 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
|
Chris@650
|
544 << path->getEventCount() << " point(s))" << endl;
|
Chris@650
|
545
|
Chris@687
|
546 auto pathId =
|
Chris@687
|
547 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
|
Chris@687
|
548 alignmentModel->setPathFrom(pathId);
|
Chris@420
|
549
|
Chris@683
|
550 emit alignmentComplete(alignmentModelId);
|
Chris@687
|
551
|
Chris@687
|
552 ModelById::release(pathId);
|
Chris@428
|
553
|
Chris@420
|
554 } else {
|
Chris@649
|
555 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
|
Chris@649
|
556 << "failed: exit code " << exitCode << ", status " << status
|
Chris@649
|
557 << endl;
|
Chris@670
|
558 alignmentModel->setError
|
Chris@670
|
559 ("Aligner process returned non-zero exit status");
|
Chris@420
|
560 }
|
Chris@420
|
561
|
Chris@423
|
562 done:
|
Chris@670
|
563 m_pendingProcesses.erase(process);
|
Chris@423
|
564 delete process;
|
Chris@420
|
565 }
|
Chris@420
|
566
|