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