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