Chris@752
|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
|
Chris@752
|
2
|
Chris@752
|
3 /*
|
Chris@752
|
4 Sonic Visualiser
|
Chris@752
|
5 An audio file viewer and annotation editor.
|
Chris@752
|
6 Centre for Digital Music, Queen Mary, University of London.
|
Chris@752
|
7
|
Chris@752
|
8 This program is free software; you can redistribute it and/or
|
Chris@752
|
9 modify it under the terms of the GNU General Public License as
|
Chris@752
|
10 published by the Free Software Foundation; either version 2 of the
|
Chris@752
|
11 License, or (at your option) any later version. See the file
|
Chris@752
|
12 COPYING included with this distribution for more information.
|
Chris@752
|
13 */
|
Chris@752
|
14
|
Chris@752
|
15 #include "ExternalProgramAligner.h"
|
Chris@752
|
16
|
Chris@752
|
17 #include <QFileInfo>
|
Chris@752
|
18 #include <QApplication>
|
Chris@752
|
19
|
Chris@752
|
20 #include "data/model/ReadOnlyWaveFileModel.h"
|
Chris@752
|
21 #include "data/model/SparseTimeValueModel.h"
|
Chris@752
|
22 #include "data/model/AlignmentModel.h"
|
Chris@752
|
23
|
Chris@752
|
24 #include "data/fileio/CSVFileReader.h"
|
Chris@752
|
25
|
Chris@752
|
26 #include "framework/Document.h"
|
Chris@752
|
27
|
Chris@752
|
28 ExternalProgramAligner::ExternalProgramAligner(Document *doc,
|
Chris@752
|
29 ModelId reference,
|
Chris@752
|
30 ModelId toAlign,
|
Chris@752
|
31 QString program) :
|
Chris@752
|
32 m_document(doc),
|
Chris@752
|
33 m_reference(reference),
|
Chris@752
|
34 m_toAlign(toAlign),
|
Chris@752
|
35 m_program(program),
|
Chris@752
|
36 m_process(nullptr)
|
Chris@752
|
37 {
|
Chris@752
|
38 }
|
Chris@752
|
39
|
Chris@752
|
40 ExternalProgramAligner::~ExternalProgramAligner()
|
Chris@752
|
41 {
|
Chris@773
|
42 if (m_process) {
|
Chris@773
|
43 disconnect(m_process, nullptr, this, nullptr);
|
Chris@773
|
44 }
|
Chris@773
|
45
|
Chris@752
|
46 delete m_process;
|
Chris@752
|
47 }
|
Chris@752
|
48
|
Chris@752
|
49 bool
|
Chris@752
|
50 ExternalProgramAligner::isAvailable(QString program)
|
Chris@752
|
51 {
|
Chris@752
|
52 QFileInfo file(program);
|
Chris@752
|
53 return file.exists() && file.isExecutable();
|
Chris@752
|
54 }
|
Chris@752
|
55
|
Chris@761
|
56 void
|
Chris@761
|
57 ExternalProgramAligner::begin()
|
Chris@752
|
58 {
|
Chris@752
|
59 // Run an external program, passing to it paths to the main
|
Chris@752
|
60 // model's audio file and the new model's audio file. It returns
|
Chris@752
|
61 // the path in CSV form through stdout.
|
Chris@752
|
62
|
Chris@752
|
63 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(m_reference);
|
Chris@752
|
64 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(m_toAlign);
|
Chris@752
|
65 if (!reference || !other) {
|
Chris@752
|
66 SVCERR << "ERROR: ExternalProgramAligner: Can't align non-read-only models via program (no local filename available)" << endl;
|
Chris@761
|
67 return;
|
Chris@752
|
68 }
|
Chris@752
|
69
|
Chris@769
|
70 if (m_program == "") {
|
Chris@769
|
71 emit failed(m_toAlign, tr("No external program specified"));
|
Chris@769
|
72 return;
|
Chris@769
|
73 }
|
Chris@769
|
74
|
Chris@752
|
75 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
|
Chris@752
|
76 qApp->processEvents();
|
Chris@752
|
77 }
|
Chris@752
|
78
|
Chris@752
|
79 QString refPath = reference->getLocalFilename();
|
Chris@752
|
80 if (refPath == "") {
|
Chris@752
|
81 refPath = FileSource(reference->getLocation()).getLocalFilename();
|
Chris@752
|
82 }
|
Chris@752
|
83
|
Chris@752
|
84 QString otherPath = other->getLocalFilename();
|
Chris@752
|
85 if (otherPath == "") {
|
Chris@752
|
86 otherPath = FileSource(other->getLocation()).getLocalFilename();
|
Chris@752
|
87 }
|
Chris@752
|
88
|
Chris@752
|
89 if (refPath == "" || otherPath == "") {
|
Chris@761
|
90 emit failed(m_toAlign,
|
Chris@761
|
91 tr("Failed to find local filepath for wave-file model"));
|
Chris@761
|
92 return;
|
Chris@752
|
93 }
|
Chris@752
|
94
|
Chris@752
|
95 auto alignmentModel =
|
Chris@752
|
96 std::make_shared<AlignmentModel>(m_reference, m_toAlign, ModelId());
|
Chris@752
|
97
|
Chris@752
|
98 m_alignmentModel = ModelById::add(alignmentModel);
|
Chris@752
|
99 other->setAlignment(m_alignmentModel);
|
Chris@752
|
100
|
Chris@752
|
101 m_process = new QProcess;
|
Chris@779
|
102 m_process->setProcessChannelMode(QProcess::SeparateChannels);
|
Chris@752
|
103
|
Chris@752
|
104 connect(m_process,
|
Chris@752
|
105 SIGNAL(finished(int, QProcess::ExitStatus)),
|
Chris@752
|
106 this,
|
Chris@752
|
107 SLOT(programFinished(int, QProcess::ExitStatus)));
|
Chris@752
|
108
|
Chris@779
|
109 connect(m_process,
|
Chris@779
|
110 SIGNAL(readyReadStandardError()),
|
Chris@779
|
111 this,
|
Chris@779
|
112 SLOT(logStderrOutput()));
|
Chris@779
|
113
|
Chris@752
|
114 QStringList args;
|
Chris@752
|
115 args << refPath << otherPath;
|
Chris@752
|
116
|
Chris@752
|
117 SVCERR << "ExternalProgramAligner: Starting program \""
|
Chris@752
|
118 << m_program << "\" with args: ";
|
Chris@752
|
119 for (auto a: args) {
|
Chris@752
|
120 SVCERR << "\"" << a << "\" ";
|
Chris@752
|
121 }
|
Chris@752
|
122 SVCERR << endl;
|
Chris@752
|
123
|
Chris@752
|
124 m_process->start(m_program, args);
|
Chris@752
|
125
|
Chris@752
|
126 bool success = m_process->waitForStarted();
|
Chris@752
|
127
|
Chris@752
|
128 if (!success) {
|
Chris@752
|
129
|
Chris@752
|
130 SVCERR << "ERROR: ExternalProgramAligner: Program did not start" << endl;
|
Chris@769
|
131
|
Chris@769
|
132 other->setAlignment({});
|
Chris@769
|
133 ModelById::release(m_alignmentModel);
|
Chris@769
|
134 delete m_process;
|
Chris@769
|
135 m_process = nullptr;
|
Chris@769
|
136
|
Chris@761
|
137 emit failed(m_toAlign,
|
Chris@761
|
138 tr("Alignment program \"%1\" did not start")
|
Chris@761
|
139 .arg(m_program));
|
Chris@752
|
140
|
Chris@752
|
141 } else {
|
Chris@769
|
142 alignmentModel->setCompletion(10);
|
Chris@752
|
143 m_document->addNonDerivedModel(m_alignmentModel);
|
Chris@752
|
144 }
|
Chris@752
|
145 }
|
Chris@752
|
146
|
Chris@752
|
147 void
|
Chris@779
|
148 ExternalProgramAligner::logStderrOutput()
|
Chris@779
|
149 {
|
Chris@779
|
150 if (!m_process) return;
|
Chris@779
|
151
|
Chris@779
|
152 m_process->setReadChannel(QProcess::StandardError);
|
Chris@779
|
153
|
Chris@779
|
154 qint64 byteCount = m_process->bytesAvailable();
|
Chris@779
|
155 if (byteCount == 0) {
|
Chris@779
|
156 m_process->setReadChannel(QProcess::StandardOutput);
|
Chris@779
|
157 return;
|
Chris@779
|
158 }
|
Chris@779
|
159
|
Chris@779
|
160 QByteArray buffer = m_process->read(byteCount);
|
Chris@779
|
161 while (buffer.endsWith('\n') || buffer.endsWith('\r')) {
|
Chris@779
|
162 buffer.chop(1);
|
Chris@779
|
163 }
|
Chris@779
|
164
|
Chris@779
|
165 QString str = QString::fromUtf8(buffer);
|
Chris@779
|
166
|
Chris@779
|
167 cerr << str << endl;
|
Chris@779
|
168
|
Chris@784
|
169 #if (QT_VERSION >= 0x050300)
|
Chris@779
|
170 QString pfx = QString("[pid%1] ").arg(m_process->processId());
|
Chris@784
|
171 #else
|
Chris@784
|
172 QString pfx = QString("[subproc] ");
|
Chris@784
|
173 #endif
|
Chris@779
|
174 str.replace("\r", "\\r");
|
Chris@779
|
175 str.replace("\n", "\n" + pfx);
|
Chris@779
|
176
|
Chris@779
|
177 SVDEBUG << pfx << str << endl;
|
Chris@779
|
178
|
Chris@779
|
179 m_process->setReadChannel(QProcess::StandardOutput);
|
Chris@779
|
180 }
|
Chris@779
|
181
|
Chris@779
|
182 void
|
Chris@769
|
183 ExternalProgramAligner::programFinished(int exitCode,
|
Chris@769
|
184 QProcess::ExitStatus status)
|
Chris@752
|
185 {
|
Chris@752
|
186 SVCERR << "ExternalProgramAligner::programFinished" << endl;
|
Chris@752
|
187
|
Chris@752
|
188 QProcess *process = qobject_cast<QProcess *>(sender());
|
Chris@752
|
189
|
Chris@752
|
190 if (process != m_process) {
|
Chris@752
|
191 SVCERR << "ERROR: ExternalProgramAligner: Emitting process " << process
|
Chris@752
|
192 << " is not my process!" << endl;
|
Chris@752
|
193 return;
|
Chris@752
|
194 }
|
Chris@752
|
195
|
Chris@779
|
196 logStderrOutput();
|
Chris@779
|
197
|
Chris@752
|
198 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
|
Chris@752
|
199 if (!alignmentModel) {
|
Chris@752
|
200 SVCERR << "ExternalProgramAligner: AlignmentModel no longer exists"
|
Chris@752
|
201 << endl;
|
Chris@752
|
202 return;
|
Chris@752
|
203 }
|
Chris@769
|
204
|
Chris@769
|
205 QString errorText;
|
Chris@752
|
206
|
Chris@752
|
207 if (exitCode == 0 && status == 0) {
|
Chris@752
|
208
|
Chris@752
|
209 CSVFormat format;
|
Chris@752
|
210 format.setModelType(CSVFormat::TwoDimensionalModel);
|
Chris@752
|
211 format.setTimingType(CSVFormat::ExplicitTiming);
|
Chris@752
|
212 format.setTimeUnits(CSVFormat::TimeSeconds);
|
Chris@752
|
213 format.setColumnCount(2);
|
Chris@752
|
214 // The output format has time in the reference file first, and
|
Chris@752
|
215 // time in the "other" file in the second column. This is a
|
Chris@752
|
216 // more natural approach for a command-line alignment tool,
|
Chris@752
|
217 // but it's the opposite of what we expect for native
|
Chris@752
|
218 // alignment paths, which map from "other" file to
|
Chris@752
|
219 // reference. These column purpose settings reflect that.
|
Chris@752
|
220 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
|
Chris@752
|
221 format.setColumnPurpose(0, CSVFormat::ColumnValue);
|
Chris@752
|
222 format.setAllowQuoting(false);
|
Chris@752
|
223 format.setSeparator(',');
|
Chris@752
|
224
|
Chris@752
|
225 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
|
Chris@752
|
226 if (!reader.isOK()) {
|
Chris@752
|
227 SVCERR << "ERROR: ExternalProgramAligner: Failed to parse output"
|
Chris@752
|
228 << endl;
|
Chris@769
|
229 errorText = tr("Failed to parse output of program: %1")
|
Chris@761
|
230 .arg(reader.getError());
|
Chris@769
|
231 alignmentModel->setError(errorText);
|
Chris@752
|
232 goto done;
|
Chris@752
|
233 }
|
Chris@752
|
234
|
Chris@752
|
235 //!!! to use ById?
|
Chris@752
|
236
|
Chris@752
|
237 Model *csvOutput = reader.load();
|
Chris@752
|
238
|
Chris@752
|
239 SparseTimeValueModel *path =
|
Chris@752
|
240 qobject_cast<SparseTimeValueModel *>(csvOutput);
|
Chris@752
|
241 if (!path) {
|
Chris@752
|
242 SVCERR << "ERROR: ExternalProgramAligner: Output did not convert to sparse time-value model"
|
Chris@752
|
243 << endl;
|
Chris@769
|
244 errorText =
|
Chris@761
|
245 tr("Output of alignment program was not in the proper format");
|
Chris@769
|
246 alignmentModel->setError(errorText);
|
Chris@752
|
247 delete csvOutput;
|
Chris@752
|
248 goto done;
|
Chris@752
|
249 }
|
Chris@752
|
250
|
Chris@752
|
251 if (path->isEmpty()) {
|
Chris@752
|
252 SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings"
|
Chris@752
|
253 << endl;
|
Chris@769
|
254 errorText =
|
Chris@761
|
255 tr("Output of alignment program contained no mappings");
|
Chris@769
|
256 alignmentModel->setError(errorText);
|
Chris@752
|
257 delete path;
|
Chris@752
|
258 goto done;
|
Chris@752
|
259 }
|
Chris@752
|
260
|
Chris@752
|
261 SVCERR << "ExternalProgramAligner: Setting alignment path ("
|
Chris@752
|
262 << path->getEventCount() << " point(s))" << endl;
|
Chris@752
|
263
|
Chris@752
|
264 auto pathId =
|
Chris@752
|
265 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
|
Chris@752
|
266 alignmentModel->setPathFrom(pathId);
|
Chris@769
|
267 alignmentModel->setCompletion(100);
|
Chris@752
|
268
|
Chris@752
|
269 ModelById::release(pathId);
|
Chris@752
|
270
|
Chris@752
|
271 } else {
|
Chris@752
|
272 SVCERR << "ERROR: ExternalProgramAligner: Aligner program "
|
Chris@752
|
273 << "failed: exit code " << exitCode << ", status " << status
|
Chris@752
|
274 << endl;
|
Chris@769
|
275 errorText = tr("Aligner process returned non-zero exit status");
|
Chris@769
|
276 alignmentModel->setError(errorText);
|
Chris@752
|
277 }
|
Chris@752
|
278
|
Chris@752
|
279 done:
|
Chris@752
|
280 delete m_process;
|
Chris@752
|
281 m_process = nullptr;
|
Chris@769
|
282
|
Chris@769
|
283 // "This should be emitted as the last thing the aligner does, as
|
Chris@769
|
284 // the recipient may delete the aligner during the call."
|
Chris@769
|
285 if (errorText == "") {
|
Chris@769
|
286 emit complete(m_alignmentModel);
|
Chris@769
|
287 } else {
|
Chris@769
|
288 emit failed(m_toAlign, errorText);
|
Chris@769
|
289 }
|
Chris@752
|
290 }
|