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@706
|
407 transform.setParameter("noise", true);
|
Chris@706
|
408 transform.setParameter("minfreq", 500);
|
Chris@420
|
409
|
Chris@704
|
410 int cents = 0;
|
Chris@704
|
411
|
Chris@670
|
412 if (tuningFrequency != 0.f) {
|
Chris@670
|
413 transform.setParameter("freq2", tuningFrequency);
|
Chris@704
|
414
|
Chris@704
|
415 double centsOffset = 0.f;
|
Chris@704
|
416 int pitch = Pitch::getPitchForFrequency(tuningFrequency, ¢sOffset);
|
Chris@704
|
417 cents = int(round((pitch - 69) * 100 + centsOffset));
|
Chris@704
|
418 SVCERR << "frequency " << tuningFrequency << " yields cents offset " << centsOffset << " and pitch " << pitch << " -> cents " << cents << endl;
|
Chris@670
|
419 }
|
Chris@670
|
420
|
Chris@704
|
421 alignmentModel->setRelativePitch(cents);
|
Chris@704
|
422
|
Chris@420
|
423 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
|
Chris@420
|
424
|
Chris@420
|
425 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
|
Chris@420
|
426
|
Chris@420
|
427 QString message;
|
Chris@691
|
428 ModelId pathOutputModelId = mtf->transform
|
Chris@683
|
429 (transform, aggregateModelId, message);
|
Chris@420
|
430
|
Chris@691
|
431 if (pathOutputModelId.isNone()) {
|
Chris@420
|
432 transform.setStepSize(0);
|
Chris@691
|
433 pathOutputModelId = mtf->transform
|
Chris@683
|
434 (transform, aggregateModelId, message);
|
Chris@420
|
435 }
|
Chris@420
|
436
|
Chris@691
|
437 auto pathOutputModel =
|
Chris@691
|
438 ModelById::getAs<SparseTimeValueModel>(pathOutputModelId);
|
Chris@420
|
439
|
Chris@670
|
440 //!!! callers will need to be updated to get error from
|
Chris@670
|
441 //!!! alignment model after initial call
|
Chris@670
|
442
|
Chris@691
|
443 if (!pathOutputModel) {
|
Chris@649
|
444 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
|
Chris@670
|
445 alignmentModel->setError(message);
|
Chris@420
|
446 return false;
|
Chris@420
|
447 }
|
Chris@420
|
448
|
Chris@691
|
449 pathOutputModel->setCompletion(0);
|
Chris@691
|
450 alignmentModel->setPathFrom(pathOutputModelId);
|
Chris@691
|
451
|
Chris@691
|
452 m_pendingAlignments[alignmentModelId] = pathOutputModelId;
|
Chris@420
|
453
|
Chris@687
|
454 connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
455 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@420
|
456
|
Chris@420
|
457 return true;
|
Chris@420
|
458 }
|
Chris@420
|
459
|
Chris@428
|
460 void
|
Chris@691
|
461 Align::alignmentCompletionChanged(ModelId alignmentModelId)
|
Chris@428
|
462 {
|
Chris@670
|
463 QMutexLocker locker (&m_mutex);
|
Chris@683
|
464
|
Chris@691
|
465 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@691
|
466
|
Chris@691
|
467 if (alignmentModel && alignmentModel->isReady()) {
|
Chris@691
|
468
|
Chris@691
|
469 if (m_pendingAlignments.find(alignmentModelId) !=
|
Chris@691
|
470 m_pendingAlignments.end()) {
|
Chris@691
|
471 ModelId pathOutputModelId = m_pendingAlignments[alignmentModelId];
|
Chris@691
|
472 ModelById::release(pathOutputModelId);
|
Chris@691
|
473 m_pendingAlignments.erase(alignmentModelId);
|
Chris@691
|
474 }
|
Chris@691
|
475
|
Chris@691
|
476 disconnect(alignmentModel.get(),
|
Chris@691
|
477 SIGNAL(completionChanged(ModelId)),
|
Chris@687
|
478 this, SLOT(alignmentCompletionChanged(ModelId)));
|
Chris@691
|
479 emit alignmentComplete(alignmentModelId);
|
Chris@428
|
480 }
|
Chris@428
|
481 }
|
Chris@428
|
482
|
Chris@420
|
483 bool
|
Chris@691
|
484 Align::alignModelViaProgram(Document *doc,
|
Chris@687
|
485 ModelId referenceId,
|
Chris@687
|
486 ModelId otherId,
|
Chris@687
|
487 QString program,
|
Chris@687
|
488 QString &error)
|
Chris@420
|
489 {
|
Chris@420
|
490 // Run an external program, passing to it paths to the main
|
Chris@420
|
491 // model's audio file and the new model's audio file. It returns
|
Chris@420
|
492 // the path in CSV form through stdout.
|
Chris@420
|
493
|
Chris@687
|
494 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(referenceId);
|
Chris@687
|
495 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(otherId);
|
Chris@687
|
496 if (!reference || !other) {
|
Chris@649
|
497 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
|
Chris@515
|
498 return false;
|
Chris@515
|
499 }
|
Chris@687
|
500
|
Chris@687
|
501 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
|
Chris@687
|
502 qApp->processEvents();
|
Chris@687
|
503 }
|
Chris@515
|
504
|
Chris@687
|
505 QString refPath = reference->getLocalFilename();
|
Chris@716
|
506 if (refPath == "") {
|
Chris@716
|
507 refPath = FileSource(reference->getLocation()).getLocalFilename();
|
Chris@716
|
508 }
|
Chris@716
|
509
|
Chris@687
|
510 QString otherPath = other->getLocalFilename();
|
Chris@716
|
511 if (otherPath == "") {
|
Chris@716
|
512 otherPath = FileSource(other->getLocation()).getLocalFilename();
|
Chris@716
|
513 }
|
Chris@420
|
514
|
Chris@420
|
515 if (refPath == "" || otherPath == "") {
|
Chris@670
|
516 error = "Failed to find local filepath for wave-file model";
|
Chris@595
|
517 return false;
|
Chris@420
|
518 }
|
Chris@420
|
519
|
Chris@718
|
520 QProcess *process = nullptr;
|
Chris@718
|
521 ModelId alignmentModelId = {};
|
Chris@718
|
522
|
Chris@718
|
523 {
|
Chris@718
|
524 QMutexLocker locker (&m_mutex);
|
Chris@423
|
525
|
Chris@718
|
526 auto alignmentModel =
|
Chris@718
|
527 std::make_shared<AlignmentModel>(referenceId, otherId, ModelId());
|
Chris@718
|
528
|
Chris@718
|
529 alignmentModelId = ModelById::add(alignmentModel);
|
Chris@718
|
530 other->setAlignment(alignmentModelId);
|
Chris@718
|
531
|
Chris@718
|
532 process = new QProcess;
|
Chris@718
|
533 process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
|
Chris@718
|
534
|
Chris@718
|
535 connect(process,
|
Chris@718
|
536 SIGNAL(finished(int, QProcess::ExitStatus)),
|
Chris@718
|
537 this,
|
Chris@718
|
538 SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
|
Chris@718
|
539
|
Chris@718
|
540 m_pendingProcesses[process] = alignmentModelId;
|
Chris@718
|
541 }
|
Chris@717
|
542
|
Chris@420
|
543 QStringList args;
|
Chris@420
|
544 args << refPath << otherPath;
|
Chris@717
|
545
|
Chris@717
|
546 SVCERR << "Align::alignModelViaProgram: Starting program \""
|
Chris@717
|
547 << program << "\" with args: ";
|
Chris@717
|
548 for (auto a: args) {
|
Chris@717
|
549 SVCERR << "\"" << a << "\" ";
|
Chris@717
|
550 }
|
Chris@717
|
551 SVCERR << endl;
|
Chris@717
|
552
|
Chris@423
|
553 process->start(program, args);
|
Chris@420
|
554
|
Chris@423
|
555 bool success = process->waitForStarted();
|
Chris@423
|
556
|
Chris@718
|
557 {
|
Chris@718
|
558 QMutexLocker locker(&m_mutex);
|
Chris@718
|
559
|
Chris@718
|
560 if (!success) {
|
Chris@718
|
561
|
Chris@718
|
562 SVCERR << "ERROR: Align::alignModelViaProgram: "
|
Chris@718
|
563 << "Program did not start" << endl;
|
Chris@718
|
564 error = "Alignment program \"" + program + "\" did not start";
|
Chris@718
|
565
|
Chris@718
|
566 m_pendingProcesses.erase(process);
|
Chris@718
|
567 other->setAlignment({});
|
Chris@718
|
568 ModelById::release(alignmentModelId);
|
Chris@718
|
569 delete process;
|
Chris@718
|
570
|
Chris@718
|
571 } else {
|
Chris@718
|
572 doc->addNonDerivedModel(alignmentModelId);
|
Chris@718
|
573 }
|
Chris@423
|
574 }
|
Chris@423
|
575
|
Chris@423
|
576 return success;
|
Chris@423
|
577 }
|
Chris@423
|
578
|
Chris@423
|
579 void
|
Chris@423
|
580 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
|
Chris@423
|
581 {
|
Chris@670
|
582 QMutexLocker locker (&m_mutex);
|
Chris@670
|
583
|
Chris@649
|
584 SVCERR << "Align::alignmentProgramFinished" << endl;
|
Chris@423
|
585
|
Chris@423
|
586 QProcess *process = qobject_cast<QProcess *>(sender());
|
Chris@423
|
587
|
Chris@670
|
588 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
|
Chris@649
|
589 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
|
Chris@649
|
590 << " not found in process model map!" << endl;
|
Chris@423
|
591 return;
|
Chris@423
|
592 }
|
Chris@423
|
593
|
Chris@683
|
594 ModelId alignmentModelId = m_pendingProcesses[process];
|
Chris@683
|
595 auto alignmentModel = ModelById::getAs<AlignmentModel>(alignmentModelId);
|
Chris@683
|
596 if (!alignmentModel) return;
|
Chris@423
|
597
|
Chris@423
|
598 if (exitCode == 0 && status == 0) {
|
Chris@420
|
599
|
Chris@595
|
600 CSVFormat format;
|
Chris@595
|
601 format.setModelType(CSVFormat::TwoDimensionalModel);
|
Chris@595
|
602 format.setTimingType(CSVFormat::ExplicitTiming);
|
Chris@595
|
603 format.setTimeUnits(CSVFormat::TimeSeconds);
|
Chris@595
|
604 format.setColumnCount(2);
|
Chris@425
|
605 // The output format has time in the reference file first, and
|
Chris@425
|
606 // time in the "other" file in the second column. This is a
|
Chris@425
|
607 // more natural approach for a command-line alignment tool,
|
Chris@425
|
608 // but it's the opposite of what we expect for native
|
Chris@425
|
609 // alignment paths, which map from "other" file to
|
Chris@425
|
610 // reference. These column purpose settings reflect that.
|
Chris@595
|
611 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
|
Chris@595
|
612 format.setColumnPurpose(0, CSVFormat::ColumnValue);
|
Chris@595
|
613 format.setAllowQuoting(false);
|
Chris@595
|
614 format.setSeparator(',');
|
Chris@420
|
615
|
Chris@595
|
616 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
|
Chris@595
|
617 if (!reader.isOK()) {
|
Chris@649
|
618 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
|
Chris@649
|
619 << endl;
|
Chris@670
|
620 alignmentModel->setError
|
Chris@670
|
621 (QString("Failed to parse output of program: %1")
|
Chris@670
|
622 .arg(reader.getError()));
|
Chris@423
|
623 goto done;
|
Chris@595
|
624 }
|
Chris@420
|
625
|
Chris@683
|
626 //!!! to use ById?
|
Chris@683
|
627
|
Chris@595
|
628 Model *csvOutput = reader.load();
|
Chris@420
|
629
|
Chris@687
|
630 SparseTimeValueModel *path =
|
Chris@687
|
631 qobject_cast<SparseTimeValueModel *>(csvOutput);
|
Chris@595
|
632 if (!path) {
|
Chris@649
|
633 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
|
Chris@649
|
634 << endl;
|
Chris@670
|
635 alignmentModel->setError
|
Chris@670
|
636 ("Output of program did not produce sparse time-value model");
|
Chris@683
|
637 delete csvOutput;
|
Chris@423
|
638 goto done;
|
Chris@595
|
639 }
|
Chris@683
|
640
|
Chris@649
|
641 if (path->isEmpty()) {
|
Chris@649
|
642 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
|
Chris@649
|
643 << endl;
|
Chris@670
|
644 alignmentModel->setError
|
Chris@670
|
645 ("Output of alignment program contained no mappings");
|
Chris@683
|
646 delete path;
|
Chris@423
|
647 goto done;
|
Chris@595
|
648 }
|
Chris@420
|
649
|
Chris@649
|
650 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
|
Chris@650
|
651 << path->getEventCount() << " point(s))" << endl;
|
Chris@650
|
652
|
Chris@687
|
653 auto pathId =
|
Chris@687
|
654 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
|
Chris@687
|
655 alignmentModel->setPathFrom(pathId);
|
Chris@420
|
656
|
Chris@683
|
657 emit alignmentComplete(alignmentModelId);
|
Chris@687
|
658
|
Chris@687
|
659 ModelById::release(pathId);
|
Chris@428
|
660
|
Chris@420
|
661 } else {
|
Chris@649
|
662 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
|
Chris@649
|
663 << "failed: exit code " << exitCode << ", status " << status
|
Chris@649
|
664 << endl;
|
Chris@670
|
665 alignmentModel->setError
|
Chris@670
|
666 ("Aligner process returned non-zero exit status");
|
Chris@420
|
667 }
|
Chris@420
|
668
|
Chris@423
|
669 done:
|
Chris@670
|
670 m_pendingProcesses.erase(process);
|
Chris@423
|
671 delete process;
|
Chris@420
|
672 }
|
Chris@420
|
673
|