comparison framework/Align.cpp @ 673:d62fd61082a1

Merge from branch tuning-difference
author Chris Cannam
date Fri, 17 May 2019 09:46:22 +0100
parents ae7584dbd668
children b375fdbb74bc
comparison
equal deleted inserted replaced
665:e19c609a7bec 673:d62fd61082a1
2 2
3 /* 3 /*
4 Sonic Visualiser 4 Sonic Visualiser
5 An audio file viewer and annotation editor. 5 An audio file viewer and annotation editor.
6 Centre for Digital Music, Queen Mary, University of London. 6 Centre for Digital Music, Queen Mary, University of London.
7 This file copyright 2006 Chris Cannam and QMUL.
8 7
9 This program is free software; you can redistribute it and/or 8 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License as 9 modify it under the terms of the GNU General Public License as
11 published by the Free Software Foundation; either version 2 of the 10 published by the Free Software Foundation; either version 2 of the
12 License, or (at your option) any later version. See the file 11 License, or (at your option) any later version. See the file
32 #include <QProcess> 31 #include <QProcess>
33 #include <QSettings> 32 #include <QSettings>
34 #include <QApplication> 33 #include <QApplication>
35 34
36 bool 35 bool
37 Align::alignModel(Document *doc, Model *ref, Model *other) 36 Align::alignModel(Document *doc, Model *ref, Model *other, QString &error)
38 { 37 {
39 QSettings settings; 38 QSettings settings;
40 settings.beginGroup("Preferences"); 39 settings.beginGroup("Preferences");
41 bool useProgram = settings.value("use-external-alignment", false).toBool(); 40 bool useProgram = settings.value("use-external-alignment", false).toBool();
42 QString program = settings.value("external-alignment-program", "").toString(); 41 QString program = settings.value("external-alignment-program", "").toString();
43 settings.endGroup(); 42 settings.endGroup();
44 43
45 if (useProgram && (program != "")) { 44 if (useProgram && (program != "")) {
46 return alignModelViaProgram(doc, ref, other, program); 45 return alignModelViaProgram(doc, ref, other, program, error);
47 } else { 46 } else {
48 return alignModelViaTransform(doc, ref, other); 47 return alignModelViaTransform(doc, ref, other, error);
49 } 48 }
50 } 49 }
51 50
52 QString 51 QString
53 Align::getAlignmentTransformName() 52 Align::getAlignmentTransformName()
59 "vamp:match-vamp-plugin:match:path").toString(); 58 "vamp:match-vamp-plugin:match:path").toString();
60 settings.endGroup(); 59 settings.endGroup();
61 return id; 60 return id;
62 } 61 }
63 62
63 QString
64 Align::getTuningDifferenceTransformName()
65 {
66 QSettings settings;
67 settings.beginGroup("Alignment");
68 bool performPitchCompensation =
69 settings.value("align-pitch-aware", false).toBool();
70 QString id = "";
71 if (performPitchCompensation) {
72 id = settings.value
73 ("tuning-difference-transform-id",
74 "vamp:tuning-difference:tuning-difference:tuningfreq")
75 .toString();
76 }
77 settings.endGroup();
78 return id;
79 }
80
64 bool 81 bool
65 Align::canAlign() 82 Align::canAlign()
66 { 83 {
84 TransformFactory *factory = TransformFactory::getInstance();
67 TransformId id = getAlignmentTransformName(); 85 TransformId id = getAlignmentTransformName();
68 TransformFactory *factory = TransformFactory::getInstance(); 86 TransformId tdId = getTuningDifferenceTransformName();
69 return factory->haveTransform(id); 87 return factory->haveTransform(id) &&
88 (tdId == "" || factory->haveTransform(tdId));
70 } 89 }
71 90
72 bool 91 bool
73 Align::alignModelViaTransform(Document *doc, Model *ref, Model *other) 92 Align::alignModelViaTransform(Document *doc, Model *ref, Model *other,
74 { 93 QString &error)
94 {
95 QMutexLocker locker (&m_mutex);
96
75 RangeSummarisableTimeValueModel *reference = qobject_cast 97 RangeSummarisableTimeValueModel *reference = qobject_cast
76 <RangeSummarisableTimeValueModel *>(ref); 98 <RangeSummarisableTimeValueModel *>(ref);
77 99
78 RangeSummarisableTimeValueModel *rm = qobject_cast 100 RangeSummarisableTimeValueModel *rm = qobject_cast
79 <RangeSummarisableTimeValueModel *>(other); 101 <RangeSummarisableTimeValueModel *>(other);
80 102
81 if (!reference || !rm) return false; // but this should have been tested already 103 if (!reference || !rm) return false; // but this should have been tested already
82 104
83 // This involves creating three new models: 105 // This involves creating either three or four new models:
84 106 //
85 // 1. an AggregateWaveModel to provide the mixdowns of the main 107 // 1. an AggregateWaveModel to provide the mixdowns of the main
86 // model and the new model in its two channels, as input to the 108 // model and the new model in its two channels, as input to the
87 // MATCH plugin 109 // MATCH plugin
88 110 //
89 // 2. a SparseTimeValueModel, which is the model automatically 111 // 2a. a SparseTimeValueModel which will be automatically created
90 // created by FeatureExtractionPluginTransformer when running the 112 // by FeatureExtractionModelTransformer when running the
91 // MATCH plugin (thus containing the alignment path) 113 // TuningDifference plugin to receive the relative tuning of the
92 114 // second model (if pitch-aware alignment is enabled in the
115 // preferences)
116 //
117 // 2b. a SparseTimeValueModel which will be automatically created
118 // by FeatureExtractionPluginTransformer when running the MATCH
119 // plugin to perform alignment (so containing the alignment path)
120 //
93 // 3. an AlignmentModel, which stores the path model and carries 121 // 3. an AlignmentModel, which stores the path model and carries
94 // out alignment lookups on it. 122 // out alignment lookups on it.
95 123 //
96 // The first two of these are provided as arguments to the 124 // The AggregateWaveModel [1] is registered with the document,
97 // constructor for the third, which takes responsibility for 125 // which deletes it when it is invalidated (when one of its
98 // deleting them. The AlignmentModel, meanwhile, is passed to the 126 // components is deleted). The SparseTimeValueModel [2a] is reused
99 // new model we are aligning, which also takes responsibility for 127 // by us when starting the alignment process proper, and is then
100 // it. We should not have to delete any of these new models here. 128 // deleted by us. The SparseTimeValueModel [2b] is passed to the
129 // AlignmentModel, which takes ownership of it. The AlignmentModel
130 // is attached to the new model we are aligning, which also takes
131 // ownership of it. The only one of these models that we need to
132 // delete here is the SparseTimeValueModel [2a].
133 //
134 // (We also create a sneaky additional SparseTimeValueModel
135 // temporarily so we can attach completion information to it -
136 // this is quite unnecessary from the perspective of simply
137 // producing the results.)
101 138
102 AggregateWaveModel::ChannelSpecList components; 139 AggregateWaveModel::ChannelSpecList components;
103 140
104 components.push_back(AggregateWaveModel::ModelChannelSpec 141 components.push_back(AggregateWaveModel::ModelChannelSpec
105 (reference, -1)); 142 (reference, -1));
107 components.push_back(AggregateWaveModel::ModelChannelSpec 144 components.push_back(AggregateWaveModel::ModelChannelSpec
108 (rm, -1)); 145 (rm, -1));
109 146
110 AggregateWaveModel *aggregateModel = new AggregateWaveModel(components); 147 AggregateWaveModel *aggregateModel = new AggregateWaveModel(components);
111 doc->addAggregateModel(aggregateModel); 148 doc->addAggregateModel(aggregateModel);
112 149
113 ModelTransformer::Input aggregate(aggregateModel); 150 AlignmentModel *alignmentModel =
114 151 new AlignmentModel(reference, other, nullptr);
152
153 TransformId tdId = getTuningDifferenceTransformName();
154
155 if (tdId == "") {
156
157 if (beginTransformDrivenAlignment(aggregateModel, alignmentModel)) {
158 rm->setAlignment(alignmentModel);
159 } else {
160 error = alignmentModel->getError();
161 delete alignmentModel;
162 return false;
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", 50);
176 transform.setParameter("maxrange", 5);
177
178 SVDEBUG << "Align::alignModel: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
179
180 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
181
182 QString message;
183 Model *transformOutput = mtf->transform(transform, aggregateModel, message);
184
185 SparseTimeValueModel *tdout = dynamic_cast<SparseTimeValueModel *>
186 (transformOutput);
187
188 if (!tdout) {
189 SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
190 delete tdout;
191 error = message;
192 return false;
193 }
194
195 rm->setAlignment(alignmentModel);
196
197 connect(tdout, SIGNAL(completionChanged()),
198 this, SLOT(tuningDifferenceCompletionChanged()));
199
200 TuningDiffRec rec;
201 rec.input = aggregateModel;
202 rec.alignment = alignmentModel;
203
204 // This model exists only so that the AlignmentModel can get a
205 // completion value from somewhere while the tuning difference
206 // calculation is going on
207 rec.preparatory = new SparseTimeValueModel
208 (aggregateModel->getSampleRate(), 1);;
209 rec.preparatory->setCompletion(0);
210 alignmentModel->setPathFrom(rec.preparatory);
211
212 m_pendingTuningDiffs[tdout] = rec;
213 }
214
215 return true;
216 }
217
218 void
219 Align::tuningDifferenceCompletionChanged()
220 {
221 QMutexLocker locker (&m_mutex);
222
223 SparseTimeValueModel *td = qobject_cast<SparseTimeValueModel *>(sender());
224 if (!td) return;
225
226 if (m_pendingTuningDiffs.find(td) == m_pendingTuningDiffs.end()) {
227 SVCERR << "ERROR: Align::tuningDifferenceCompletionChanged: Model "
228 << td << " not found in pending tuning diff map!" << endl;
229 return;
230 }
231
232 TuningDiffRec rec = m_pendingTuningDiffs[td];
233
234 int completion = 0;
235 bool done = td->isReady(&completion);
236
237 SVCERR << "Align::tuningDifferenceCompletionChanged: done = " << done << ", completion = " << completion << endl;
238
239 if (!done) {
240 // This will be the completion the alignment model reports,
241 // before the alignment actually begins. It goes up from 0 to
242 // 99 (not 100!) and then back to 0 again when we start
243 // calculating the actual path in the following phase
244 int clamped = (completion == 100 ? 99 : completion);
245 SVCERR << "Align::tuningDifferenceCompletionChanged: setting rec.preparatory completion to " << clamped << endl;
246 rec.preparatory->setCompletion(clamped);
247 return;
248 }
249
250 float tuningFrequency = 440.f;
251
252 if (!td->isEmpty()) {
253 tuningFrequency = td->getAllEvents()[0].getValue();
254 SVCERR << "Align::tuningDifferenceCompletionChanged: Reported tuning frequency = " << tuningFrequency << endl;
255 } else {
256 SVCERR << "Align::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
257 }
258
259 m_pendingTuningDiffs.erase(td);
260 td->aboutToDelete();
261 delete td;
262
263 rec.alignment->setPathFrom(nullptr);
264
265 beginTransformDrivenAlignment
266 (rec.input, rec.alignment, tuningFrequency);
267 }
268
269 bool
270 Align::beginTransformDrivenAlignment(AggregateWaveModel *aggregateModel,
271 AlignmentModel *alignmentModel,
272 float tuningFrequency)
273 {
115 TransformId id = getAlignmentTransformName(); 274 TransformId id = getAlignmentTransformName();
116 275
117 TransformFactory *tf = TransformFactory::getInstance(); 276 TransformFactory *tf = TransformFactory::getInstance();
118 277
119 Transform transform = tf->getDefaultTransformFor 278 Transform transform = tf->getDefaultTransformFor
121 280
122 transform.setStepSize(transform.getBlockSize()/2); 281 transform.setStepSize(transform.getBlockSize()/2);
123 transform.setParameter("serialise", 1); 282 transform.setParameter("serialise", 1);
124 transform.setParameter("smooth", 0); 283 transform.setParameter("smooth", 0);
125 284
285 if (tuningFrequency != 0.f) {
286 transform.setParameter("freq2", tuningFrequency);
287 }
288
126 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; 289 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
127 290
128 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); 291 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
129 292
130 QString message; 293 QString message;
131 Model *transformOutput = mtf->transform(transform, aggregate, message); 294 Model *transformOutput = mtf->transform
295 (transform, aggregateModel, message);
132 296
133 if (!transformOutput) { 297 if (!transformOutput) {
134 transform.setStepSize(0); 298 transform.setStepSize(0);
135 transformOutput = mtf->transform(transform, aggregate, message); 299 transformOutput = mtf->transform
300 (transform, aggregateModel, message);
136 } 301 }
137 302
138 SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *> 303 SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *>
139 (transformOutput); 304 (transformOutput);
140 305
306 //!!! callers will need to be updated to get error from
307 //!!! alignment model after initial call
308
141 if (!path) { 309 if (!path) {
142 cerr << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl; 310 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
143 delete transformOutput; 311 delete transformOutput;
144 delete aggregateModel; 312 alignmentModel->setError(message);
145 m_error = message;
146 return false; 313 return false;
147 } 314 }
148 315
149 path->setCompletion(0); 316 path->setCompletion(0);
150 317 alignmentModel->setPathFrom(path);
151 AlignmentModel *alignmentModel = new AlignmentModel
152 (reference, other, path);
153 318
154 connect(alignmentModel, SIGNAL(completionChanged()), 319 connect(alignmentModel, SIGNAL(completionChanged()),
155 this, SLOT(alignmentCompletionChanged())); 320 this, SLOT(alignmentCompletionChanged()));
156
157 rm->setAlignment(alignmentModel);
158 321
159 return true; 322 return true;
160 } 323 }
161 324
162 void 325 void
163 Align::alignmentCompletionChanged() 326 Align::alignmentCompletionChanged()
164 { 327 {
328 QMutexLocker locker (&m_mutex);
329
165 AlignmentModel *am = qobject_cast<AlignmentModel *>(sender()); 330 AlignmentModel *am = qobject_cast<AlignmentModel *>(sender());
166 if (!am) return; 331 if (!am) return;
167 if (am->isReady()) { 332 if (am->isReady()) {
168 disconnect(am, SIGNAL(completionChanged()), 333 disconnect(am, SIGNAL(completionChanged()),
169 this, SLOT(alignmentCompletionChanged())); 334 this, SLOT(alignmentCompletionChanged()));
170 emit alignmentComplete(am); 335 emit alignmentComplete(am);
171 } 336 }
172 } 337 }
173 338
174 bool 339 bool
175 Align::alignModelViaProgram(Document *, Model *ref, Model *other, QString program) 340 Align::alignModelViaProgram(Document *, Model *ref, Model *other,
176 { 341 QString program, QString &error)
342 {
343 QMutexLocker locker (&m_mutex);
344
177 WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref); 345 WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref);
178 WaveFileModel *rm = qobject_cast<WaveFileModel *>(other); 346 WaveFileModel *rm = qobject_cast<WaveFileModel *>(other);
179 347
180 if (!reference || !rm) { 348 if (!reference || !rm) {
181 return false; // but this should have been tested already 349 return false; // but this should have been tested already
190 // the path in CSV form through stdout. 358 // the path in CSV form through stdout.
191 359
192 ReadOnlyWaveFileModel *roref = qobject_cast<ReadOnlyWaveFileModel *>(reference); 360 ReadOnlyWaveFileModel *roref = qobject_cast<ReadOnlyWaveFileModel *>(reference);
193 ReadOnlyWaveFileModel *rorm = qobject_cast<ReadOnlyWaveFileModel *>(rm); 361 ReadOnlyWaveFileModel *rorm = qobject_cast<ReadOnlyWaveFileModel *>(rm);
194 if (!roref || !rorm) { 362 if (!roref || !rorm) {
195 cerr << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl; 363 SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
196 return false; 364 return false;
197 } 365 }
198 366
199 QString refPath = roref->getLocalFilename(); 367 QString refPath = roref->getLocalFilename();
200 QString otherPath = rorm->getLocalFilename(); 368 QString otherPath = rorm->getLocalFilename();
201 369
202 if (refPath == "" || otherPath == "") { 370 if (refPath == "" || otherPath == "") {
203 m_error = "Failed to find local filepath for wave-file model"; 371 error = "Failed to find local filepath for wave-file model";
204 return false; 372 return false;
205 } 373 }
206 374
207 m_error = "";
208
209 AlignmentModel *alignmentModel = 375 AlignmentModel *alignmentModel =
210 new AlignmentModel(reference, other, nullptr); 376 new AlignmentModel(reference, other, nullptr);
211 rm->setAlignment(alignmentModel); 377 rm->setAlignment(alignmentModel);
212 378
213 QProcess *process = new QProcess; 379 QProcess *process = new QProcess;
215 args << refPath << otherPath; 381 args << refPath << otherPath;
216 382
217 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), 383 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
218 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus))); 384 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
219 385
220 m_processModels[process] = alignmentModel; 386 m_pendingProcesses[process] = alignmentModel;
221 process->start(program, args); 387 process->start(program, args);
222 388
223 bool success = process->waitForStarted(); 389 bool success = process->waitForStarted();
224 390
225 if (!success) { 391 if (!success) {
226 cerr << "ERROR: Align::alignModelViaProgram: Program did not start" 392 SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
227 << endl; 393 << endl;
228 m_error = "Alignment program could not be started"; 394 error = "Alignment program could not be started";
229 m_processModels.erase(process); 395 m_pendingProcesses.erase(process);
230 rm->setAlignment(nullptr); // deletes alignmentModel as well 396 rm->setAlignment(nullptr); // deletes alignmentModel as well
231 delete process; 397 delete process;
232 } 398 }
233 399
234 return success; 400 return success;
235 } 401 }
236 402
237 void 403 void
238 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status) 404 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
239 { 405 {
240 cerr << "Align::alignmentProgramFinished" << endl; 406 QMutexLocker locker (&m_mutex);
407
408 SVCERR << "Align::alignmentProgramFinished" << endl;
241 409
242 QProcess *process = qobject_cast<QProcess *>(sender()); 410 QProcess *process = qobject_cast<QProcess *>(sender());
243 411
244 if (m_processModels.find(process) == m_processModels.end()) { 412 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
245 cerr << "ERROR: Align::alignmentProgramFinished: Process " << process 413 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
246 << " not found in process model map!" << endl; 414 << " not found in process model map!" << endl;
247 return; 415 return;
248 } 416 }
249 417
250 AlignmentModel *alignmentModel = m_processModels[process]; 418 AlignmentModel *alignmentModel = m_pendingProcesses[process];
251 419
252 if (exitCode == 0 && status == 0) { 420 if (exitCode == 0 && status == 0) {
253 421
254 CSVFormat format; 422 CSVFormat format;
255 format.setModelType(CSVFormat::TwoDimensionalModel); 423 format.setModelType(CSVFormat::TwoDimensionalModel);
267 format.setAllowQuoting(false); 435 format.setAllowQuoting(false);
268 format.setSeparator(','); 436 format.setSeparator(',');
269 437
270 CSVFileReader reader(process, format, alignmentModel->getSampleRate()); 438 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
271 if (!reader.isOK()) { 439 if (!reader.isOK()) {
272 cerr << "ERROR: Align::alignmentProgramFinished: Failed to parse output" 440 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
273 << endl; 441 << endl;
274 m_error = QString("Failed to parse output of program: %1") 442 alignmentModel->setError
275 .arg(reader.getError()); 443 (QString("Failed to parse output of program: %1")
444 .arg(reader.getError()));
276 goto done; 445 goto done;
277 } 446 }
278 447
279 Model *csvOutput = reader.load(); 448 Model *csvOutput = reader.load();
280 449
281 SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput); 450 SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput);
282 if (!path) { 451 if (!path) {
283 cerr << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model" 452 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
284 << endl; 453 << endl;
285 m_error = QString("Output of program did not produce sparse time-value model"); 454 alignmentModel->setError
455 ("Output of program did not produce sparse time-value model");
286 goto done; 456 goto done;
287 } 457 }
288 458
289 if (path->getPoints().empty()) { 459 if (path->isEmpty()) {
290 cerr << "ERROR: Align::alignmentProgramFinished: Output contained no mappings" 460 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
291 << endl; 461 << endl;
292 m_error = QString("Output of alignment program contained no mappings"); 462 alignmentModel->setError
463 ("Output of alignment program contained no mappings");
293 goto done; 464 goto done;
294 } 465 }
295 466
296 cerr << "Align::alignmentProgramFinished: Setting alignment path (" 467 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
297 << path->getPoints().size() << " point(s))" << endl; 468 << path->getEventCount() << " point(s))" << endl;
298 469
299 alignmentModel->setPathFrom(path); 470 alignmentModel->setPathFrom(path);
300 471
301 emit alignmentComplete(alignmentModel); 472 emit alignmentComplete(alignmentModel);
302 473
303 } else { 474 } else {
304 cerr << "ERROR: Align::alignmentProgramFinished: Aligner program " 475 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
305 << "failed: exit code " << exitCode << ", status " << status 476 << "failed: exit code " << exitCode << ", status " << status
306 << endl; 477 << endl;
307 m_error = "Aligner process returned non-zero exit status"; 478 alignmentModel->setError
479 ("Aligner process returned non-zero exit status");
308 } 480 }
309 481
310 done: 482 done:
311 m_processModels.erase(process); 483 m_pendingProcesses.erase(process);
312 delete process; 484 delete process;
313 } 485 }
314 486