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