Mercurial > hg > svapp
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 ¢sOffset); | |
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 } |