comparison align/TransformAligner.cpp @ 752:32654e402f8b pitch-align

Pull out ExternalProgramAligner and TransformAligner from Align - currently duplicating the code, the pulled-out classes are not yet in use
author Chris Cannam
date Thu, 23 Apr 2020 17:11:26 +0100
parents
children 31289e8592c7
comparison
equal deleted inserted replaced
751:ed5db7d37005 752:32654e402f8b
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 "TransformAligner.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 TransformAligner::TransformAligner(Document *doc,
31 ModelId reference,
32 ModelId toAlign) :
33 m_document(doc),
34 m_reference(reference),
35 m_toAlign(toAlign),
36 m_tuningFrequency(440.f),
37 m_incomplete(true)
38 {
39 }
40
41 TransformAligner::~TransformAligner()
42 {
43 if (m_incomplete) {
44 auto other =
45 ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
46 if (other) {
47 other->setAlignment({});
48 }
49 }
50
51 ModelById::release(m_tuningDiffProgressModel);
52 ModelById::release(m_tuningDiffOutputModel);
53 ModelById::release(m_pathOutputModel);
54 }
55
56 QString
57 TransformAligner::getAlignmentTransformName()
58 {
59 QSettings settings;
60 settings.beginGroup("Alignment");
61 TransformId id =
62 settings.value("transform-id",
63 "vamp:match-vamp-plugin:match:path").toString();
64 settings.endGroup();
65 return id;
66 }
67
68 QString
69 TransformAligner::getTuningDifferenceTransformName()
70 {
71 QSettings settings;
72 settings.beginGroup("Alignment");
73 bool performPitchCompensation =
74 settings.value("align-pitch-aware", false).toBool();
75 QString id = "";
76 if (performPitchCompensation) {
77 id = settings.value
78 ("tuning-difference-transform-id",
79 "vamp:tuning-difference:tuning-difference:tuningfreq")
80 .toString();
81 }
82 settings.endGroup();
83 return id;
84 }
85
86 bool
87 TransformAligner::isAvailable()
88 {
89 TransformFactory *factory = TransformFactory::getInstance();
90 TransformId id = getAlignmentTransformName();
91 TransformId tdId = getTuningDifferenceTransformName();
92 return factory->haveTransform(id) &&
93 (tdId == "" || factory->haveTransform(tdId));
94 }
95
96 bool
97 TransformAligner::begin(QString &error)
98 {
99 auto reference =
100 ModelById::getAs<RangeSummarisableTimeValueModel>(m_reference);
101 auto other =
102 ModelById::getAs<RangeSummarisableTimeValueModel>(m_toAlign);
103
104 if (!reference || !other) return false;
105
106 // This involves creating a number of new models:
107 //
108 // 1. an AggregateWaveModel to provide the mixdowns of the main
109 // model and the new model in its two channels, as input to the
110 // MATCH plugin. We just call this one aggregateModel
111 //
112 // 2a. a SparseTimeValueModel which will be automatically created
113 // by FeatureExtractionModelTransformer when running the
114 // TuningDifference plugin to receive the relative tuning of the
115 // second model (if pitch-aware alignment is enabled in the
116 // preferences). This is m_tuningDiffOutputModel.
117 //
118 // 2b. a SparseTimeValueModel which will be automatically created
119 // by FeatureExtractionPluginTransformer when running the MATCH
120 // plugin to perform alignment (so containing the alignment path).
121 // This is m_pathOutputModel.
122 //
123 // 2c. a SparseTimeValueModel used solely to provide faked
124 // completion information to the AlignmentModel while a
125 // TuningDifference calculation is going on. We call this
126 // m_tuningDiffProgressModel.
127 //
128 // 3. an AlignmentModel, which stores the path and carries out
129 // alignment lookups on it. This one is m_alignmentModel.
130 //
131 // Models 1 and 3 are registered with the document, which will
132 // eventually release them. We don't release them here except in
133 // the case where an activity fails before the point where we
134 // would otherwise have registered them with the document.
135 //
136 // Models 2a (m_tuningDiffOutputModel), 2b (m_pathOutputModel) and
137 // 2c (m_tuningDiffProgressModel) are not registered with the
138 // document, because they are not intended to persist, and also
139 // Model 2c (m_tuningDiffProgressModel) is a bodge that we are
140 // embarrassed about, so we try to manage it ourselves without
141 // anyone else noticing. These have to be released by us when
142 // finished with, but their lifespans do not extend beyond the end
143 // of the alignment procedure, so this should be ok.
144
145 AggregateWaveModel::ChannelSpecList components;
146 components.push_back
147 (AggregateWaveModel::ModelChannelSpec(m_reference, -1));
148
149 components.push_back
150 (AggregateWaveModel::ModelChannelSpec(m_toAlign, -1));
151
152 auto aggregateModel = std::make_shared<AggregateWaveModel>(components);
153 m_aggregateModel = ModelById::add(aggregateModel);
154 m_document->addNonDerivedModel(m_aggregateModel);
155
156 auto alignmentModel = std::make_shared<AlignmentModel>
157 (m_reference, m_toAlign, ModelId());
158 m_alignmentModel = ModelById::add(alignmentModel);
159
160 TransformId tdId = getTuningDifferenceTransformName();
161
162 if (tdId == "") {
163
164 if (beginAlignmentPhase()) {
165 other->setAlignment(m_alignmentModel);
166 m_document->addNonDerivedModel(m_alignmentModel);
167 } else {
168 error = alignmentModel->getError();
169 ModelById::release(alignmentModel);
170 return false;
171 }
172
173 } else {
174
175 // Have a tuning-difference transform id, so run it
176 // asynchronously first
177
178 TransformFactory *tf = TransformFactory::getInstance();
179
180 Transform transform = tf->getDefaultTransformFor
181 (tdId, aggregateModel->getSampleRate());
182
183 transform.setParameter("maxduration", 60);
184 transform.setParameter("maxrange", 6);
185 transform.setParameter("finetuning", false);
186
187 SVDEBUG << "TransformAligner: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
188
189 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
190
191 QString message;
192 ModelId tuningDiffOutputModelId = mtf->transform(transform,
193 m_aggregateModel,
194 message);
195
196 auto tuningDiffOutputModel =
197 ModelById::getAs<SparseTimeValueModel>(tuningDiffOutputModelId);
198 if (!tuningDiffOutputModel) {
199 SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
200 error = message;
201 ModelById::release(alignmentModel);
202 return false;
203 }
204
205 other->setAlignment(m_alignmentModel);
206 m_document->addNonDerivedModel(m_alignmentModel);
207
208 connect(tuningDiffOutputModel.get(),
209 SIGNAL(completionChanged(ModelId)),
210 this, SLOT(tuningDifferenceCompletionChanged(ModelId)));
211
212 // This model exists only so that the AlignmentModel can get a
213 // completion value from somewhere while the tuning difference
214 // calculation is going on
215 auto progressModel = std::make_shared<SparseTimeValueModel>
216 (aggregateModel->getSampleRate(), 1);
217 m_tuningDiffProgressModel = ModelById::add(progressModel);
218 progressModel->setCompletion(0);
219 alignmentModel->setPathFrom(m_tuningDiffProgressModel);
220 }
221
222 return true;
223 }
224
225 void
226 TransformAligner::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId)
227 {
228 if (tuningDiffOutputModelId != m_tuningDiffOutputModel) {
229 SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged: Model "
230 << tuningDiffOutputModelId
231 << " is not ours!" << endl;
232 return;
233 }
234
235 auto tuningDiffOutputModel =
236 ModelById::getAs<SparseTimeValueModel>(m_tuningDiffOutputModel);
237 if (!tuningDiffOutputModel) {
238 SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged: Model "
239 << tuningDiffOutputModelId
240 << " not known as SparseTimeValueModel" << endl;
241 return;
242 }
243
244 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
245 if (!alignmentModel) {
246 SVCERR << "WARNING: TransformAligner::tuningDifferenceCompletionChanged:"
247 << "alignment model has disappeared" << endl;
248 return;
249 }
250
251 int completion = 0;
252 bool done = tuningDiffOutputModel->isReady(&completion);
253
254 if (!done) {
255 // This will be the completion the alignment model reports,
256 // before the alignment actually begins. It goes up from 0 to
257 // 99 (not 100!) and then back to 0 again when we start
258 // calculating the actual path in the following phase
259 int clamped = (completion == 100 ? 99 : completion);
260 auto progressModel =
261 ModelById::getAs<SparseTimeValueModel>(m_tuningDiffProgressModel);
262 if (progressModel) {
263 progressModel->setCompletion(clamped);
264 }
265 return;
266 }
267
268 m_tuningFrequency = 440.f;
269
270 if (!tuningDiffOutputModel->isEmpty()) {
271 m_tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue();
272 SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: Reported tuning frequency = " << m_tuningFrequency << endl;
273 } else {
274 SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
275 }
276
277 ModelById::release(tuningDiffOutputModel);
278 m_tuningDiffOutputModel = {};
279
280 alignmentModel->setPathFrom({}); // replace m_tuningDiffProgressModel
281 ModelById::release(m_tuningDiffProgressModel);
282 m_tuningDiffProgressModel = {};
283
284 beginAlignmentPhase();
285 }
286
287 bool
288 TransformAligner::beginAlignmentPhase()
289 {
290 TransformId id = getAlignmentTransformName();
291
292 TransformFactory *tf = TransformFactory::getInstance();
293
294 auto aggregateModel =
295 ModelById::getAs<AggregateWaveModel>(m_aggregateModel);
296 auto alignmentModel =
297 ModelById::getAs<AlignmentModel>(m_alignmentModel);
298
299 if (!aggregateModel || !alignmentModel) {
300 SVCERR << "TransformAligner::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl;
301 return false;
302 }
303
304 Transform transform = tf->getDefaultTransformFor
305 (id, aggregateModel->getSampleRate());
306
307 transform.setStepSize(transform.getBlockSize()/2);
308 transform.setParameter("serialise", 1);
309 transform.setParameter("smooth", 0);
310 transform.setParameter("zonewidth", 40);
311 transform.setParameter("noise", true);
312 transform.setParameter("minfreq", 500);
313
314 int cents = 0;
315
316 if (m_tuningFrequency != 0.f) {
317 transform.setParameter("freq2", m_tuningFrequency);
318
319 double centsOffset = 0.f;
320 int pitch = Pitch::getPitchForFrequency(m_tuningFrequency,
321 &centsOffset);
322 cents = int(round((pitch - 69) * 100 + centsOffset));
323 SVCERR << "TransformAligner: frequency " << m_tuningFrequency
324 << " yields cents offset " << centsOffset
325 << " and pitch " << pitch << " -> cents " << cents << endl;
326 }
327
328 alignmentModel->setRelativePitch(cents);
329
330 SVDEBUG << "Align::alignModel: Alignment transform step size "
331 << transform.getStepSize() << ", block size "
332 << transform.getBlockSize() << endl;
333
334 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
335
336 QString message;
337 ModelId pathOutputModelId = mtf->transform
338 (transform, m_aggregateModel, message);
339
340 if (pathOutputModelId.isNone()) {
341 transform.setStepSize(0);
342 pathOutputModelId = mtf->transform
343 (transform, m_aggregateModel, message);
344 }
345
346 auto pathOutputModel =
347 ModelById::getAs<SparseTimeValueModel>(pathOutputModelId);
348
349 //!!! callers will need to be updated to get error from
350 //!!! alignment model after initial call
351
352 if (!pathOutputModel) {
353 SVCERR << "TransformAligner: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
354 alignmentModel->setError(message);
355 return false;
356 }
357
358 pathOutputModel->setCompletion(0);
359 alignmentModel->setPathFrom(m_pathOutputModel);
360
361 connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)),
362 this, SLOT(alignmentCompletionChanged(ModelId)));
363
364 return true;
365 }
366
367 void
368 TransformAligner::alignmentCompletionChanged(ModelId alignmentModelId)
369 {
370 if (alignmentModelId != m_alignmentModel) {
371 SVCERR << "WARNING: TransformAligner::alignmentCompletionChanged: Model "
372 << alignmentModelId
373 << " is not ours!" << endl;
374 return;
375 }
376
377 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
378
379 if (alignmentModel && alignmentModel->isReady()) {
380
381 m_incomplete = false;
382
383 ModelById::release(m_pathOutputModel);
384 m_pathOutputModel = {};
385
386 disconnect(alignmentModel.get(),
387 SIGNAL(completionChanged(ModelId)),
388 this, SLOT(alignmentCompletionChanged(ModelId)));
389 emit complete(m_alignmentModel);
390 }
391 }