comparison framework/Align.cpp @ 670:0960e27c3232 tuning-difference

Experiment with optionally taking tuning difference into account for alignment
author Chris Cannam
date Wed, 15 May 2019 17:52:22 +0100
parents 21673429dba5
children b6cafe05017d
comparison
equal deleted inserted replaced
669:331be52cd473 670:0960e27c3232
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
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)
92 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].
101 133
102 AggregateWaveModel::ChannelSpecList components; 134 AggregateWaveModel::ChannelSpecList components;
103 135
104 components.push_back(AggregateWaveModel::ModelChannelSpec 136 components.push_back(AggregateWaveModel::ModelChannelSpec
105 (reference, -1)); 137 (reference, -1));
107 components.push_back(AggregateWaveModel::ModelChannelSpec 139 components.push_back(AggregateWaveModel::ModelChannelSpec
108 (rm, -1)); 140 (rm, -1));
109 141
110 AggregateWaveModel *aggregateModel = new AggregateWaveModel(components); 142 AggregateWaveModel *aggregateModel = new AggregateWaveModel(components);
111 doc->addAggregateModel(aggregateModel); 143 doc->addAggregateModel(aggregateModel);
112 144
113 ModelTransformer::Input aggregate(aggregateModel); 145 AlignmentModel *alignmentModel =
114 146 new AlignmentModel(reference, other, nullptr);
147
148 connect(alignmentModel, SIGNAL(completionChanged()),
149 this, SLOT(alignmentCompletionChanged()));
150
151 TransformId tdId = getTuningDifferenceTransformName();
152
153 if (tdId == "") {
154
155 if (beginTransformDrivenAlignment(aggregateModel, alignmentModel)) {
156 rm->setAlignment(alignmentModel);
157 } else {
158 error = alignmentModel->getError();
159 delete alignmentModel;
160 return false;
161 }
162
163 } else {
164
165 // Have a tuning-difference transform id, so run it
166 // asynchronously first
167
168 TransformFactory *tf = TransformFactory::getInstance();
169
170 Transform transform = tf->getDefaultTransformFor
171 (tdId, aggregateModel->getSampleRate());
172
173 SVDEBUG << "Align::alignModel: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
174
175 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
176
177 QString message;
178 Model *transformOutput = mtf->transform(transform, aggregateModel, message);
179
180 SparseTimeValueModel *tdout = dynamic_cast<SparseTimeValueModel *>
181 (transformOutput);
182
183 if (!tdout) {
184 SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl;
185 delete tdout;
186 error = message;
187 return false;
188 }
189
190 rm->setAlignment(alignmentModel);
191
192 connect(tdout, SIGNAL(completionChanged()),
193 this, SLOT(tuningDifferenceCompletionChanged()));
194
195 m_pendingTuningDiffs[tdout] =
196 std::pair<AggregateWaveModel *, AlignmentModel *>
197 (aggregateModel, alignmentModel);
198 }
199
200 return true;
201 }
202
203 bool
204 Align::beginTransformDrivenAlignment(AggregateWaveModel *aggregateModel,
205 AlignmentModel *alignmentModel,
206 float tuningFrequency)
207 {
115 TransformId id = getAlignmentTransformName(); 208 TransformId id = getAlignmentTransformName();
116 209
117 TransformFactory *tf = TransformFactory::getInstance(); 210 TransformFactory *tf = TransformFactory::getInstance();
118 211
119 Transform transform = tf->getDefaultTransformFor 212 Transform transform = tf->getDefaultTransformFor
121 214
122 transform.setStepSize(transform.getBlockSize()/2); 215 transform.setStepSize(transform.getBlockSize()/2);
123 transform.setParameter("serialise", 1); 216 transform.setParameter("serialise", 1);
124 transform.setParameter("smooth", 0); 217 transform.setParameter("smooth", 0);
125 218
219 if (tuningFrequency != 0.f) {
220 transform.setParameter("freq2", tuningFrequency);
221 }
222
126 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; 223 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
127 224
128 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); 225 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
129 226
130 QString message; 227 QString message;
131 Model *transformOutput = mtf->transform(transform, aggregate, message); 228 Model *transformOutput = mtf->transform
229 (transform, aggregateModel, message);
132 230
133 if (!transformOutput) { 231 if (!transformOutput) {
134 transform.setStepSize(0); 232 transform.setStepSize(0);
135 transformOutput = mtf->transform(transform, aggregate, message); 233 transformOutput = mtf->transform
234 (transform, aggregateModel, message);
136 } 235 }
137 236
138 SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *> 237 SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *>
139 (transformOutput); 238 (transformOutput);
140 239
240 //!!! callers will need to be updated to get error from
241 //!!! alignment model after initial call
242
141 if (!path) { 243 if (!path) {
142 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl; 244 SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
143 delete transformOutput; 245 delete transformOutput;
144 delete aggregateModel; 246 alignmentModel->setError(message);
145 m_error = message;
146 return false; 247 return false;
147 } 248 }
148 249
149 path->setCompletion(0); 250 path->setCompletion(0);
150 251 alignmentModel->setPathFrom(path);
151 AlignmentModel *alignmentModel = new AlignmentModel
152 (reference, other, path);
153 252
154 connect(alignmentModel, SIGNAL(completionChanged()), 253 connect(alignmentModel, SIGNAL(completionChanged()),
155 this, SLOT(alignmentCompletionChanged())); 254 this, SLOT(alignmentCompletionChanged()));
156
157 rm->setAlignment(alignmentModel);
158 255
159 return true; 256 return true;
257 }
258
259 void
260 Align::tuningDifferenceCompletionChanged()
261 {
262 QMutexLocker locker (&m_mutex);
263
264 SparseTimeValueModel *td = qobject_cast<SparseTimeValueModel *>(sender());
265 if (!td) return;
266 if (!td->isReady()) return;
267
268 disconnect(td, SIGNAL(completionChanged()),
269 this, SLOT(alignmentCompletionChanged()));
270
271 if (m_pendingTuningDiffs.find(td) == m_pendingTuningDiffs.end()) {
272 SVCERR << "ERROR: Align::tuningDifferenceCompletionChanged: Model "
273 << td << " not found in pending tuning diff map!" << endl;
274 return;
275 }
276
277 std::pair<AggregateWaveModel *, AlignmentModel *> models =
278 m_pendingTuningDiffs[td];
279
280 float tuningFrequency = 440.f;
281
282 if (!td->isEmpty()) {
283 tuningFrequency = td->getAllEvents()[0].getValue();
284 SVCERR << "Align::tuningDifferenceCompletionChanged: Reported tuning frequency = " << tuningFrequency << endl;
285 } else {
286 SVCERR << "Align::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl;
287 }
288
289 m_pendingTuningDiffs.erase(td);
290
291 beginTransformDrivenAlignment
292 (models.first, models.second, tuningFrequency);
160 } 293 }
161 294
162 void 295 void
163 Align::alignmentCompletionChanged() 296 Align::alignmentCompletionChanged()
164 { 297 {
298 QMutexLocker locker (&m_mutex);
299
165 AlignmentModel *am = qobject_cast<AlignmentModel *>(sender()); 300 AlignmentModel *am = qobject_cast<AlignmentModel *>(sender());
166 if (!am) return; 301 if (!am) return;
167 if (am->isReady()) { 302 if (am->isReady()) {
168 disconnect(am, SIGNAL(completionChanged()), 303 disconnect(am, SIGNAL(completionChanged()),
169 this, SLOT(alignmentCompletionChanged())); 304 this, SLOT(alignmentCompletionChanged()));
170 emit alignmentComplete(am); 305 emit alignmentComplete(am);
171 } 306 }
172 } 307 }
173 308
174 bool 309 bool
175 Align::alignModelViaProgram(Document *, Model *ref, Model *other, QString program) 310 Align::alignModelViaProgram(Document *, Model *ref, Model *other,
176 { 311 QString program, QString &error)
312 {
313 QMutexLocker locker (&m_mutex);
314
177 WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref); 315 WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref);
178 WaveFileModel *rm = qobject_cast<WaveFileModel *>(other); 316 WaveFileModel *rm = qobject_cast<WaveFileModel *>(other);
179 317
180 if (!reference || !rm) { 318 if (!reference || !rm) {
181 return false; // but this should have been tested already 319 return false; // but this should have been tested already
198 336
199 QString refPath = roref->getLocalFilename(); 337 QString refPath = roref->getLocalFilename();
200 QString otherPath = rorm->getLocalFilename(); 338 QString otherPath = rorm->getLocalFilename();
201 339
202 if (refPath == "" || otherPath == "") { 340 if (refPath == "" || otherPath == "") {
203 m_error = "Failed to find local filepath for wave-file model"; 341 error = "Failed to find local filepath for wave-file model";
204 return false; 342 return false;
205 } 343 }
206 344
207 m_error = "";
208
209 AlignmentModel *alignmentModel = 345 AlignmentModel *alignmentModel =
210 new AlignmentModel(reference, other, nullptr); 346 new AlignmentModel(reference, other, nullptr);
211 rm->setAlignment(alignmentModel); 347 rm->setAlignment(alignmentModel);
212 348
213 QProcess *process = new QProcess; 349 QProcess *process = new QProcess;
215 args << refPath << otherPath; 351 args << refPath << otherPath;
216 352
217 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), 353 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
218 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus))); 354 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
219 355
220 m_processModels[process] = alignmentModel; 356 m_pendingProcesses[process] = alignmentModel;
221 process->start(program, args); 357 process->start(program, args);
222 358
223 bool success = process->waitForStarted(); 359 bool success = process->waitForStarted();
224 360
225 if (!success) { 361 if (!success) {
226 SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start" 362 SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start"
227 << endl; 363 << endl;
228 m_error = "Alignment program could not be started"; 364 error = "Alignment program could not be started";
229 m_processModels.erase(process); 365 m_pendingProcesses.erase(process);
230 rm->setAlignment(nullptr); // deletes alignmentModel as well 366 rm->setAlignment(nullptr); // deletes alignmentModel as well
231 delete process; 367 delete process;
232 } 368 }
233 369
234 return success; 370 return success;
235 } 371 }
236 372
237 void 373 void
238 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status) 374 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
239 { 375 {
376 QMutexLocker locker (&m_mutex);
377
240 SVCERR << "Align::alignmentProgramFinished" << endl; 378 SVCERR << "Align::alignmentProgramFinished" << endl;
241 379
242 QProcess *process = qobject_cast<QProcess *>(sender()); 380 QProcess *process = qobject_cast<QProcess *>(sender());
243 381
244 if (m_processModels.find(process) == m_processModels.end()) { 382 if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) {
245 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process 383 SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process
246 << " not found in process model map!" << endl; 384 << " not found in process model map!" << endl;
247 return; 385 return;
248 } 386 }
249 387
250 AlignmentModel *alignmentModel = m_processModels[process]; 388 AlignmentModel *alignmentModel = m_pendingProcesses[process];
251 389
252 if (exitCode == 0 && status == 0) { 390 if (exitCode == 0 && status == 0) {
253 391
254 CSVFormat format; 392 CSVFormat format;
255 format.setModelType(CSVFormat::TwoDimensionalModel); 393 format.setModelType(CSVFormat::TwoDimensionalModel);
269 407
270 CSVFileReader reader(process, format, alignmentModel->getSampleRate()); 408 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
271 if (!reader.isOK()) { 409 if (!reader.isOK()) {
272 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output" 410 SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
273 << endl; 411 << endl;
274 m_error = QString("Failed to parse output of program: %1") 412 alignmentModel->setError
275 .arg(reader.getError()); 413 (QString("Failed to parse output of program: %1")
414 .arg(reader.getError()));
276 goto done; 415 goto done;
277 } 416 }
278 417
279 Model *csvOutput = reader.load(); 418 Model *csvOutput = reader.load();
280 419
281 SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput); 420 SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput);
282 if (!path) { 421 if (!path) {
283 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model" 422 SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
284 << endl; 423 << endl;
285 m_error = QString("Output of program did not produce sparse time-value model"); 424 alignmentModel->setError
425 ("Output of program did not produce sparse time-value model");
286 goto done; 426 goto done;
287 } 427 }
288 428
289 if (path->isEmpty()) { 429 if (path->isEmpty()) {
290 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings" 430 SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
291 << endl; 431 << endl;
292 m_error = QString("Output of alignment program contained no mappings"); 432 alignmentModel->setError
433 ("Output of alignment program contained no mappings");
293 goto done; 434 goto done;
294 } 435 }
295 436
296 SVCERR << "Align::alignmentProgramFinished: Setting alignment path (" 437 SVCERR << "Align::alignmentProgramFinished: Setting alignment path ("
297 << path->getEventCount() << " point(s))" << endl; 438 << path->getEventCount() << " point(s))" << endl;
302 443
303 } else { 444 } else {
304 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program " 445 SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program "
305 << "failed: exit code " << exitCode << ", status " << status 446 << "failed: exit code " << exitCode << ", status " << status
306 << endl; 447 << endl;
307 m_error = "Aligner process returned non-zero exit status"; 448 alignmentModel->setError
449 ("Aligner process returned non-zero exit status");
308 } 450 }
309 451
310 done: 452 done:
311 m_processModels.erase(process); 453 m_pendingProcesses.erase(process);
312 delete process; 454 delete process;
313 } 455 }
314 456