comparison align/MATCHAligner.cpp @ 777:87d33e79855b pitch-align

Rename TransformAligner -> MATCHAligner. It is still specific to MATCH, and at this point I think it's simpler to leave it that way and reduce confusion with the TransformDTWAligner which is much more generic.
author Chris Cannam
date Thu, 25 Jun 2020 17:43:10 +0100
parents align/TransformAligner.cpp@a316cb6fed81
children b651dc5ff555
comparison
equal deleted inserted replaced
776:32e66fcc4cb7 777:87d33e79855b
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2
3 /*
4 Sonic Visualiser
5 An audio file viewer and annotation editor.
6 Centre for Digital Music, Queen Mary, University of London.
7
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License as
10 published by the Free Software Foundation; either version 2 of the
11 License, or (at your option) any later version. See the file
12 COPYING included with this distribution for more information.
13 */
14
15 #include "MATCHAligner.h"
16
17 #include "data/model/SparseTimeValueModel.h"
18 #include "data/model/RangeSummarisableTimeValueModel.h"
19 #include "data/model/AlignmentModel.h"
20 #include "data/model/AggregateWaveModel.h"
21
22 #include "framework/Document.h"
23
24 #include "transform/TransformFactory.h"
25 #include "transform/ModelTransformerFactory.h"
26 #include "transform/FeatureExtractionModelTransformer.h"
27
28 #include <QSettings>
29
30 MATCHAligner::MATCHAligner(Document *doc,
31 ModelId reference,
32 ModelId toAlign,
33 bool withTuningDifference) :
34 m_document(doc),
35 m_reference(reference),
36 m_toAlign(toAlign),
37 m_withTuningDifference(withTuningDifference),
38 m_tuningFrequency(440.f),
39 m_incomplete(true)
40 {
41 }
42
43 MATCHAligner::~MATCHAligner()
44 {
45 if (m_incomplete) {
46 auto other =
47 ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
48 if (other) {
49 other->setAlignment({});
50 }
51 }
52
53 ModelById::release(m_tuningDiffOutputModel);
54 ModelById::release(m_pathOutputModel);
55 }
56
57 QString
58 MATCHAligner::getAlignmentTransformName()
59 {
60 QSettings settings;
61 settings.beginGroup("Alignment");
62 TransformId id = settings.value
63 ("transform-id",
64 "vamp:match-vamp-plugin:match:path").toString();
65 settings.endGroup();
66 return id;
67 }
68
69 QString
70 MATCHAligner::getTuningDifferenceTransformName()
71 {
72 QSettings settings;
73 settings.beginGroup("Alignment");
74 TransformId id = settings.value
75 ("tuning-difference-transform-id",
76 "vamp:tuning-difference:tuning-difference:tuningfreq")
77 .toString();
78 settings.endGroup();
79 return id;
80 }
81
82 bool
83 MATCHAligner::isAvailable()
84 {
85 TransformFactory *factory = TransformFactory::getInstance();
86 TransformId id = getAlignmentTransformName();
87 TransformId tdId = getTuningDifferenceTransformName();
88 return factory->haveTransform(id) &&
89 (tdId == "" || factory->haveTransform(tdId));
90 }
91
92 void
93 MATCHAligner::begin()
94 {
95 auto reference =
96 ModelById::getAs<RangeSummarisableTimeValueModel>(m_reference);
97 auto other =
98 ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
99
100 if (!reference || !other) return;
101
102 // This involves creating a number of new models:
103 //
104 // 1. an AggregateWaveModel to provide the mixdowns of the main
105 // model and the new model in its two channels, as input to the
106 // MATCH plugin. We just call this one aggregateModel
107 //
108 // 2a. a SparseTimeValueModel which will be automatically created
109 // by FeatureExtractionModelTransformer when running the
110 // TuningDifference plugin to receive the relative tuning of the
111 // second model (if pitch-aware alignment is enabled in the
112 // preferences). This is m_tuningDiffOutputModel.
113 //
114 // 2b. a SparseTimeValueModel which will be automatically created
115 // by FeatureExtractionPluginTransformer when running the MATCH
116 // plugin to perform alignment (so containing the alignment path).
117 // This is m_pathOutputModel.
118 //
119 // 3. an AlignmentModel, which stores the path and carries out
120 // alignment lookups on it. This one is m_alignmentModel.
121 //
122 // Models 1 and 3 are registered with the document, which will
123 // eventually release them. We don't release them here except in
124 // the case where an activity fails before the point where we
125 // would otherwise have registered them with the document.
126 //
127 // Models 2a (m_tuningDiffOutputModel) and 2b (m_pathOutputModel)
128 // are not registered with the document, because they are not
129 // intended to persist. These have to be released by us when
130 // finished with, but their lifespans do not extend beyond the end
131 // of the alignment procedure, so this should be ok.
132
133 AggregateWaveModel::ChannelSpecList components;
134 components.push_back
135 (AggregateWaveModel::ModelChannelSpec(m_reference, -1));
136
137 components.push_back
138 (AggregateWaveModel::ModelChannelSpec(m_toAlign, -1));
139
140 auto aggregateModel = std::make_shared<AggregateWaveModel>(components);
141 m_aggregateModel = ModelById::add(aggregateModel);
142 m_document->addNonDerivedModel(m_aggregateModel);
143
144 auto alignmentModel = std::make_shared<AlignmentModel>
145 (m_reference, m_toAlign, ModelId());
146 m_alignmentModel = ModelById::add(alignmentModel);
147
148 TransformId tdId;
149 if (m_withTuningDifference) {
150 tdId = getTuningDifferenceTransformName();
151 }
152
153 if (tdId == "") {
154
155 if (beginAlignmentPhase()) {
156 other->setAlignment(m_alignmentModel);
157 m_document->addNonDerivedModel(m_alignmentModel);
158 } else {
159 QString error = alignmentModel->getError();
160 ModelById::release(alignmentModel);
161 emit failed(m_toAlign, error);
162 return;
163 }
164
165 } else {
166
167 // Have a tuning-difference transform id, so run it
168 // asynchronously first
169
170 TransformFactory *tf = TransformFactory::getInstance();
171
172 Transform transform = tf->getDefaultTransformFor
173 (tdId, aggregateModel->getSampleRate());
174
175 transform.setParameter("maxduration", 60);
176 transform.setParameter("maxrange", 6);
177 transform.setParameter("finetuning", false);
178
179 SVDEBUG << "MATCHAligner: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
180
181 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
182
183 QString message;
184 m_tuningDiffOutputModel = mtf->transform(transform,
185 m_aggregateModel,
186 message);
187
188 auto tuningDiffOutputModel =
189 ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
190 if (!tuningDiffOutputModel) {
191 SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
192 ModelById::release(alignmentModel);
193 emit failed(m_toAlign, message);
194 return;
195 }
196
197 other->setAlignment(m_alignmentModel);
198 m_document->addNonDerivedModel(m_alignmentModel);
199
200 connect(tuningDiffOutputModel.get(),
201 SIGNAL(completionChanged(ModelId)),
202 this, SLOT(tuningDifferenceCompletionChanged(ModelId)));
203 }
204 }
205
206 void
207 MATCHAligner::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId)
208 {
209 if (m_tuningDiffOutputModel.isNone()) {
210 // we're done, this is probably a spurious queued event
211 return;
212 }
213
214 if (tuningDiffOutputModelId != m_tuningDiffOutputModel) {
215 SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged: Model "
216 << tuningDiffOutputModelId
217 << " is not ours! (ours is "
218 << m_tuningDiffOutputModel << ")" << endl;
219 return;
220 }
221
222 auto tuningDiffOutputModel =
223 ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
224 if (!tuningDiffOutputModel) {
225 SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged: Model "
226 << tuningDiffOutputModelId
227 << " not known as SparseTimeValueModel" << endl;
228 return;
229 }
230
231 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
232 if (!alignmentModel) {
233 SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged:"
234 << "alignment model has disappeared" << endl;
235 return;
236 }
237
238 int completion = 0;
239 bool done = tuningDiffOutputModel->isReady(&completion);
240
241 SVDEBUG << "MATCHAligner::tuningDifferenceCompletionChanged: model "
242 << m_tuningDiffOutputModel << ", completion = " << completion
243 << ", done = " << done << endl;
244
245 if (!done) {
246 // This will be the completion the alignment model reports,
247 // before the alignment actually begins
248 alignmentModel->setCompletion(completion / 2);
249 return;
250 }
251
252 m_tuningFrequency = 440.f;
253
254 if (!tuningDiffOutputModel->isEmpty()) {
255 m_tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue();
256 SVCERR << "MATCHAligner::tuningDifferenceCompletionChanged: Reported tuning frequency = " << m_tuningFrequency << endl;
257 } else {
258 SVCERR << "MATCHAligner::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
259 }
260
261 ModelById::release(tuningDiffOutputModel);
262 m_tuningDiffOutputModel = {};
263
264 beginAlignmentPhase();
265 }
266
267 bool
268 MATCHAligner::beginAlignmentPhase()
269 {
270 TransformId id = getAlignmentTransformName();
271
272 SVDEBUG << "MATCHAligner::beginAlignmentPhase: transform is "
273 << id << endl;
274
275 TransformFactory *tf = TransformFactory::getInstance();
276
277 auto aggregateModel =
278 ModelById::getAs<AggregateWaveModel>(m_aggregateModel);
279 auto alignmentModel =
280 ModelById::getAs<AlignmentModel>(m_alignmentModel);
281
282 if (!aggregateModel || !alignmentModel) {
283 SVCERR << "MATCHAligner::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl;
284 return false;
285 }
286
287 Transform transform = tf->getDefaultTransformFor
288 (id, aggregateModel->getSampleRate());
289
290 transform.setStepSize(transform.getBlockSize()/2);
291 transform.setParameter("serialise", 1);
292 transform.setParameter("smooth", 0);
293 transform.setParameter("zonewidth", 40);
294 transform.setParameter("noise", true);
295 transform.setParameter("minfreq", 500);
296
297 int cents = 0;
298
299 if (m_tuningFrequency != 0.f) {
300 transform.setParameter("freq2", m_tuningFrequency);
301
302 double centsOffset = 0.f;
303 int pitch = Pitch::getPitchForFrequency(m_tuningFrequency,
304 &centsOffset);
305 cents = int(round((pitch - 69) * 100 + centsOffset));
306 SVCERR << "MATCHAligner: frequency " << m_tuningFrequency
307 << " yields cents offset " << centsOffset
308 << " and pitch " << pitch << " -> cents " << cents << endl;
309 }
310
311 alignmentModel->setRelativePitch(cents);
312
313 SVDEBUG << "MATCHAligner: Alignment transform step size "
314 << transform.getStepSize() << ", block size "
315 << transform.getBlockSize() << endl;
316
317 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
318
319 QString message;
320 m_pathOutputModel = mtf->transform
321 (transform, m_aggregateModel, message);
322
323 if (m_pathOutputModel.isNone()) {
324 transform.setStepSize(0);
325 m_pathOutputModel = mtf->transform
326 (transform, m_aggregateModel, message);
327 }
328
329 auto pathOutputModel =
330 ModelById::getAs<SparseTimeValueModel>(m_pathOutputModel);
331
332 //!!! callers will need to be updated to get error from
333 //!!! alignment model after initial call
334
335 if (!pathOutputModel) {
336 SVCERR << "MATCHAligner: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
337 alignmentModel->setError(message);
338 return false;
339 }
340
341 pathOutputModel->setCompletion(0);
342 alignmentModel->setPathFrom(m_pathOutputModel);
343
344 connect(pathOutputModel.get(), SIGNAL(completionChanged(ModelId)),
345 this, SLOT(alignmentCompletionChanged(ModelId)));
346
347 return true;
348 }
349
350 void
351 MATCHAligner::alignmentCompletionChanged(ModelId pathOutputModelId)
352 {
353 if (pathOutputModelId != m_pathOutputModel) {
354 SVCERR << "WARNING: MATCHAligner::alignmentCompletionChanged: Model "
355 << pathOutputModelId
356 << " is not ours! (ours is "
357 << m_pathOutputModel << ")" << endl;
358 return;
359 }
360
361 auto pathOutputModel =
362 ModelById::getAs<SparseTimeValueModel>(m_pathOutputModel);
363 if (!pathOutputModel) {
364 SVCERR << "WARNING: MATCHAligner::alignmentCompletionChanged: Path output model "
365 << m_pathOutputModel << " no longer exists" << endl;
366 return;
367 }
368
369 int completion = 0;
370 bool done = pathOutputModel->isReady(&completion);
371
372 if (m_withTuningDifference) {
373 if (auto alignmentModel =
374 ModelById::getAs<AlignmentModel>(m_alignmentModel)) {
375 if (!done) {
376 int adjustedCompletion = 50 + completion/2;
377 if (adjustedCompletion > 99) {
378 adjustedCompletion = 99;
379 }
380 alignmentModel->setCompletion(adjustedCompletion);
381 } else {
382 alignmentModel->setCompletion(100);
383 }
384 }
385 }
386
387 if (done) {
388 m_incomplete = false;
389
390 ModelById::release(m_pathOutputModel);
391 m_pathOutputModel = {};
392
393 emit complete(m_alignmentModel);
394 }
395 }