Mercurial > hg > svapp
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 |