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@752
|
102 m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
|
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@752
|
109 QStringList args;
|
Chris@752
|
110 args << refPath << otherPath;
|
Chris@752
|
111
|
Chris@752
|
112 SVCERR << "ExternalProgramAligner: Starting program \""
|
Chris@752
|
113 << m_program << "\" with args: ";
|
Chris@752
|
114 for (auto a: args) {
|
Chris@752
|
115 SVCERR << "\"" << a << "\" ";
|
Chris@752
|
116 }
|
Chris@752
|
117 SVCERR << endl;
|
Chris@752
|
118
|
Chris@752
|
119 m_process->start(m_program, args);
|
Chris@752
|
120
|
Chris@752
|
121 bool success = m_process->waitForStarted();
|
Chris@752
|
122
|
Chris@752
|
123 if (!success) {
|
Chris@752
|
124
|
Chris@752
|
125 SVCERR << "ERROR: ExternalProgramAligner: Program did not start" << endl;
|
Chris@769
|
126
|
Chris@769
|
127 other->setAlignment({});
|
Chris@769
|
128 ModelById::release(m_alignmentModel);
|
Chris@769
|
129 delete m_process;
|
Chris@769
|
130 m_process = nullptr;
|
Chris@769
|
131
|
Chris@761
|
132 emit failed(m_toAlign,
|
Chris@761
|
133 tr("Alignment program \"%1\" did not start")
|
Chris@761
|
134 .arg(m_program));
|
Chris@752
|
135
|
Chris@752
|
136 } else {
|
Chris@769
|
137 alignmentModel->setCompletion(10);
|
Chris@752
|
138 m_document->addNonDerivedModel(m_alignmentModel);
|
Chris@752
|
139 }
|
Chris@752
|
140 }
|
Chris@752
|
141
|
Chris@752
|
142 void
|
Chris@769
|
143 ExternalProgramAligner::programFinished(int exitCode,
|
Chris@769
|
144 QProcess::ExitStatus status)
|
Chris@752
|
145 {
|
Chris@752
|
146 SVCERR << "ExternalProgramAligner::programFinished" << endl;
|
Chris@752
|
147
|
Chris@752
|
148 QProcess *process = qobject_cast<QProcess *>(sender());
|
Chris@752
|
149
|
Chris@752
|
150 if (process != m_process) {
|
Chris@752
|
151 SVCERR << "ERROR: ExternalProgramAligner: Emitting process " << process
|
Chris@752
|
152 << " is not my process!" << endl;
|
Chris@752
|
153 return;
|
Chris@752
|
154 }
|
Chris@752
|
155
|
Chris@752
|
156 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
|
Chris@752
|
157 if (!alignmentModel) {
|
Chris@752
|
158 SVCERR << "ExternalProgramAligner: AlignmentModel no longer exists"
|
Chris@752
|
159 << endl;
|
Chris@752
|
160 return;
|
Chris@752
|
161 }
|
Chris@769
|
162
|
Chris@769
|
163 QString errorText;
|
Chris@752
|
164
|
Chris@752
|
165 if (exitCode == 0 && status == 0) {
|
Chris@752
|
166
|
Chris@752
|
167 CSVFormat format;
|
Chris@752
|
168 format.setModelType(CSVFormat::TwoDimensionalModel);
|
Chris@752
|
169 format.setTimingType(CSVFormat::ExplicitTiming);
|
Chris@752
|
170 format.setTimeUnits(CSVFormat::TimeSeconds);
|
Chris@752
|
171 format.setColumnCount(2);
|
Chris@752
|
172 // The output format has time in the reference file first, and
|
Chris@752
|
173 // time in the "other" file in the second column. This is a
|
Chris@752
|
174 // more natural approach for a command-line alignment tool,
|
Chris@752
|
175 // but it's the opposite of what we expect for native
|
Chris@752
|
176 // alignment paths, which map from "other" file to
|
Chris@752
|
177 // reference. These column purpose settings reflect that.
|
Chris@752
|
178 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
|
Chris@752
|
179 format.setColumnPurpose(0, CSVFormat::ColumnValue);
|
Chris@752
|
180 format.setAllowQuoting(false);
|
Chris@752
|
181 format.setSeparator(',');
|
Chris@752
|
182
|
Chris@752
|
183 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
|
Chris@752
|
184 if (!reader.isOK()) {
|
Chris@752
|
185 SVCERR << "ERROR: ExternalProgramAligner: Failed to parse output"
|
Chris@752
|
186 << endl;
|
Chris@769
|
187 errorText = tr("Failed to parse output of program: %1")
|
Chris@761
|
188 .arg(reader.getError());
|
Chris@769
|
189 alignmentModel->setError(errorText);
|
Chris@752
|
190 goto done;
|
Chris@752
|
191 }
|
Chris@752
|
192
|
Chris@752
|
193 //!!! to use ById?
|
Chris@752
|
194
|
Chris@752
|
195 Model *csvOutput = reader.load();
|
Chris@752
|
196
|
Chris@752
|
197 SparseTimeValueModel *path =
|
Chris@752
|
198 qobject_cast<SparseTimeValueModel *>(csvOutput);
|
Chris@752
|
199 if (!path) {
|
Chris@752
|
200 SVCERR << "ERROR: ExternalProgramAligner: Output did not convert to sparse time-value model"
|
Chris@752
|
201 << endl;
|
Chris@769
|
202 errorText =
|
Chris@761
|
203 tr("Output of alignment program was not in the proper format");
|
Chris@769
|
204 alignmentModel->setError(errorText);
|
Chris@752
|
205 delete csvOutput;
|
Chris@752
|
206 goto done;
|
Chris@752
|
207 }
|
Chris@752
|
208
|
Chris@752
|
209 if (path->isEmpty()) {
|
Chris@752
|
210 SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings"
|
Chris@752
|
211 << endl;
|
Chris@769
|
212 errorText =
|
Chris@761
|
213 tr("Output of alignment program contained no mappings");
|
Chris@769
|
214 alignmentModel->setError(errorText);
|
Chris@752
|
215 delete path;
|
Chris@752
|
216 goto done;
|
Chris@752
|
217 }
|
Chris@752
|
218
|
Chris@752
|
219 SVCERR << "ExternalProgramAligner: Setting alignment path ("
|
Chris@752
|
220 << path->getEventCount() << " point(s))" << endl;
|
Chris@752
|
221
|
Chris@752
|
222 auto pathId =
|
Chris@752
|
223 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
|
Chris@752
|
224 alignmentModel->setPathFrom(pathId);
|
Chris@769
|
225 alignmentModel->setCompletion(100);
|
Chris@752
|
226
|
Chris@752
|
227 ModelById::release(pathId);
|
Chris@752
|
228
|
Chris@752
|
229 } else {
|
Chris@752
|
230 SVCERR << "ERROR: ExternalProgramAligner: Aligner program "
|
Chris@752
|
231 << "failed: exit code " << exitCode << ", status " << status
|
Chris@752
|
232 << endl;
|
Chris@769
|
233 errorText = tr("Aligner process returned non-zero exit status");
|
Chris@769
|
234 alignmentModel->setError(errorText);
|
Chris@752
|
235 }
|
Chris@752
|
236
|
Chris@752
|
237 done:
|
Chris@752
|
238 delete m_process;
|
Chris@752
|
239 m_process = nullptr;
|
Chris@769
|
240
|
Chris@769
|
241 // "This should be emitted as the last thing the aligner does, as
|
Chris@769
|
242 // the recipient may delete the aligner during the call."
|
Chris@769
|
243 if (errorText == "") {
|
Chris@769
|
244 emit complete(m_alignmentModel);
|
Chris@769
|
245 } else {
|
Chris@769
|
246 emit failed(m_toAlign, errorText);
|
Chris@769
|
247 }
|
Chris@752
|
248 }
|