comparison framework/Align.cpp @ 520:c3648c667a0b 3.0-integration

Merge from branch "alignment-simple"
author Chris Cannam
date Thu, 21 Apr 2016 15:06:43 +0100
parents 51befd6165a3
children b23bebfdfaba
comparison
equal deleted inserted replaced
518:f7ec9e410108 520:c3648c667a0b
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 This file copyright 2006 Chris Cannam and QMUL.
8
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License as
11 published by the Free Software Foundation; either version 2 of the
12 License, or (at your option) any later version. See the file
13 COPYING included with this distribution for more information.
14 */
15
16 #include "Align.h"
17
18 #include "data/model/WaveFileModel.h"
19 #include "data/model/ReadOnlyWaveFileModel.h"
20 #include "data/model/AggregateWaveModel.h"
21 #include "data/model/RangeSummarisableTimeValueModel.h"
22 #include "data/model/SparseTimeValueModel.h"
23 #include "data/model/AlignmentModel.h"
24
25 #include "data/fileio/CSVFileReader.h"
26
27 #include "transform/TransformFactory.h"
28 #include "transform/ModelTransformerFactory.h"
29 #include "transform/FeatureExtractionModelTransformer.h"
30
31 #include <QProcess>
32 #include <QSettings>
33 #include <QApplication>
34
35 bool
36 Align::alignModel(Model *ref, Model *other)
37 {
38 QSettings settings;
39 settings.beginGroup("Preferences");
40 bool useProgram = settings.value("use-external-alignment", false).toBool();
41 QString program = settings.value("external-alignment-program", "").toString();
42 settings.endGroup();
43
44 if (useProgram && (program != "")) {
45 return alignModelViaProgram(ref, other, program);
46 } else {
47 return alignModelViaTransform(ref, other);
48 }
49 }
50
51 QString
52 Align::getAlignmentTransformName()
53 {
54 QSettings settings;
55 settings.beginGroup("Alignment");
56 TransformId id =
57 settings.value("transform-id",
58 "vamp:match-vamp-plugin:match:path").toString();
59 settings.endGroup();
60 return id;
61 }
62
63 bool
64 Align::canAlign()
65 {
66 TransformId id = getAlignmentTransformName();
67 TransformFactory *factory = TransformFactory::getInstance();
68 return factory->haveTransform(id);
69 }
70
71 bool
72 Align::alignModelViaTransform(Model *ref, Model *other)
73 {
74 RangeSummarisableTimeValueModel *reference = qobject_cast
75 <RangeSummarisableTimeValueModel *>(ref);
76
77 RangeSummarisableTimeValueModel *rm = qobject_cast
78 <RangeSummarisableTimeValueModel *>(other);
79
80 if (!reference || !rm) return false; // but this should have been tested already
81
82 // This involves creating three new models:
83
84 // 1. an AggregateWaveModel to provide the mixdowns of the main
85 // model and the new model in its two channels, as input to the
86 // MATCH plugin
87
88 // 2. a SparseTimeValueModel, which is the model automatically
89 // created by FeatureExtractionPluginTransformer when running the
90 // MATCH plugin (thus containing the alignment path)
91
92 // 3. an AlignmentModel, which stores the path model and carries
93 // out alignment lookups on it.
94
95 // The first two of these are provided as arguments to the
96 // constructor for the third, which takes responsibility for
97 // deleting them. The AlignmentModel, meanwhile, is passed to the
98 // new model we are aligning, which also takes responsibility for
99 // it. We should not have to delete any of these new models here.
100
101 AggregateWaveModel::ChannelSpecList components;
102
103 components.push_back(AggregateWaveModel::ModelChannelSpec
104 (reference, -1));
105
106 components.push_back(AggregateWaveModel::ModelChannelSpec
107 (rm, -1));
108
109 Model *aggregateModel = new AggregateWaveModel(components);
110 ModelTransformer::Input aggregate(aggregateModel);
111
112 TransformId id = getAlignmentTransformName();
113
114 TransformFactory *tf = TransformFactory::getInstance();
115
116 Transform transform = tf->getDefaultTransformFor
117 (id, aggregateModel->getSampleRate());
118
119 transform.setStepSize(transform.getBlockSize()/2);
120 transform.setParameter("serialise", 1);
121 transform.setParameter("smooth", 0);
122
123 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
124
125 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
126
127 QString message;
128 Model *transformOutput = mtf->transform(transform, aggregate, message);
129
130 if (!transformOutput) {
131 transform.setStepSize(0);
132 transformOutput = mtf->transform(transform, aggregate, message);
133 }
134
135 SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *>
136 (transformOutput);
137
138 if (!path) {
139 cerr << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
140 delete transformOutput;
141 delete aggregateModel;
142 m_error = message;
143 return false;
144 }
145
146 path->setCompletion(0);
147
148 AlignmentModel *alignmentModel = new AlignmentModel
149 (reference, other, aggregateModel, path);
150
151 connect(alignmentModel, SIGNAL(completionChanged()),
152 this, SLOT(alignmentCompletionChanged()));
153
154 rm->setAlignment(alignmentModel);
155
156 return true;
157 }
158
159 void
160 Align::alignmentCompletionChanged()
161 {
162 AlignmentModel *am = qobject_cast<AlignmentModel *>(sender());
163 if (!am) return;
164 if (am->isReady()) {
165 disconnect(am, SIGNAL(completionChanged()),
166 this, SLOT(alignmentCompletionChanged()));
167 emit alignmentComplete(am);
168 }
169 }
170
171 bool
172 Align::alignModelViaProgram(Model *ref, Model *other, QString program)
173 {
174 WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref);
175 WaveFileModel *rm = qobject_cast<WaveFileModel *>(other);
176
177 if (!reference || !rm) {
178 return false; // but this should have been tested already
179 }
180
181 while (!reference->isReady(0) || !rm->isReady(0)) {
182 qApp->processEvents();
183 }
184
185 // Run an external program, passing to it paths to the main
186 // model's audio file and the new model's audio file. It returns
187 // the path in CSV form through stdout.
188
189 ReadOnlyWaveFileModel *roref = qobject_cast<ReadOnlyWaveFileModel *>(reference);
190 ReadOnlyWaveFileModel *rorm = qobject_cast<ReadOnlyWaveFileModel *>(rm);
191 if (!roref || !rorm) {
192 cerr << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
193 return false;
194 }
195
196 QString refPath = roref->getLocalFilename();
197 QString otherPath = rorm->getLocalFilename();
198
199 if (refPath == "" || otherPath == "") {
200 m_error = "Failed to find local filepath for wave-file model";
201 return false;
202 }
203
204 m_error = "";
205
206 AlignmentModel *alignmentModel = new AlignmentModel(reference, other, 0, 0);
207 rm->setAlignment(alignmentModel);
208
209 QProcess *process = new QProcess;
210 QStringList args;
211 args << refPath << otherPath;
212
213 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
214 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
215
216 m_processModels[process] = alignmentModel;
217 process->start(program, args);
218
219 bool success = process->waitForStarted();
220
221 if (!success) {
222 cerr << "ERROR: Align::alignModelViaProgram: Program did not start"
223 << endl;
224 m_error = "Alignment program could not be started";
225 m_processModels.erase(process);
226 rm->setAlignment(0); // deletes alignmentModel as well
227 delete process;
228 }
229
230 return success;
231 }
232
233 void
234 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
235 {
236 cerr << "Align::alignmentProgramFinished" << endl;
237
238 QProcess *process = qobject_cast<QProcess *>(sender());
239
240 if (m_processModels.find(process) == m_processModels.end()) {
241 cerr << "ERROR: Align::alignmentProgramFinished: Process " << process
242 << " not found in process model map!" << endl;
243 return;
244 }
245
246 AlignmentModel *alignmentModel = m_processModels[process];
247
248 if (exitCode == 0 && status == 0) {
249
250 CSVFormat format;
251 format.setModelType(CSVFormat::TwoDimensionalModel);
252 format.setTimingType(CSVFormat::ExplicitTiming);
253 format.setTimeUnits(CSVFormat::TimeSeconds);
254 format.setColumnCount(2);
255 // The output format has time in the reference file first, and
256 // time in the "other" file in the second column. This is a
257 // more natural approach for a command-line alignment tool,
258 // but it's the opposite of what we expect for native
259 // alignment paths, which map from "other" file to
260 // reference. These column purpose settings reflect that.
261 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
262 format.setColumnPurpose(0, CSVFormat::ColumnValue);
263 format.setAllowQuoting(false);
264 format.setSeparator(',');
265
266 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
267 if (!reader.isOK()) {
268 cerr << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
269 << endl;
270 m_error = QString("Failed to parse output of program: %1")
271 .arg(reader.getError());
272 goto done;
273 }
274
275 Model *csvOutput = reader.load();
276
277 SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput);
278 if (!path) {
279 cerr << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
280 << endl;
281 m_error = QString("Output of program did not produce sparse time-value model");
282 goto done;
283 }
284
285 if (path->getPoints().empty()) {
286 cerr << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
287 << endl;
288 m_error = QString("Output of alignment program contained no mappings");
289 goto done;
290 }
291
292 cerr << "Align::alignmentProgramFinished: Setting alignment path ("
293 << path->getPoints().size() << " point(s))" << endl;
294
295 alignmentModel->setPathFrom(path);
296
297 emit alignmentComplete(alignmentModel);
298
299 } else {
300 cerr << "ERROR: Align::alignmentProgramFinished: Aligner program "
301 << "failed: exit code " << exitCode << ", status " << status
302 << endl;
303 m_error = "Aligner process returned non-zero exit status";
304 }
305
306 done:
307 m_processModels.erase(process);
308 delete process;
309 }
310