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