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@752
|
42 delete m_process;
|
Chris@752
|
43 }
|
Chris@752
|
44
|
Chris@752
|
45 bool
|
Chris@752
|
46 ExternalProgramAligner::isAvailable(QString program)
|
Chris@752
|
47 {
|
Chris@752
|
48 QFileInfo file(program);
|
Chris@752
|
49 return file.exists() && file.isExecutable();
|
Chris@752
|
50 }
|
Chris@752
|
51
|
Chris@761
|
52 void
|
Chris@761
|
53 ExternalProgramAligner::begin()
|
Chris@752
|
54 {
|
Chris@752
|
55 // Run an external program, passing to it paths to the main
|
Chris@752
|
56 // model's audio file and the new model's audio file. It returns
|
Chris@752
|
57 // the path in CSV form through stdout.
|
Chris@752
|
58
|
Chris@752
|
59 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(m_reference);
|
Chris@752
|
60 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(m_toAlign);
|
Chris@752
|
61 if (!reference || !other) {
|
Chris@752
|
62 SVCERR << "ERROR: ExternalProgramAligner: Can't align non-read-only models via program (no local filename available)" << endl;
|
Chris@761
|
63 return;
|
Chris@752
|
64 }
|
Chris@752
|
65
|
Chris@769
|
66 if (m_program == "") {
|
Chris@769
|
67 emit failed(m_toAlign, tr("No external program specified"));
|
Chris@769
|
68 return;
|
Chris@769
|
69 }
|
Chris@769
|
70
|
Chris@752
|
71 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
|
Chris@752
|
72 qApp->processEvents();
|
Chris@752
|
73 }
|
Chris@752
|
74
|
Chris@752
|
75 QString refPath = reference->getLocalFilename();
|
Chris@752
|
76 if (refPath == "") {
|
Chris@752
|
77 refPath = FileSource(reference->getLocation()).getLocalFilename();
|
Chris@752
|
78 }
|
Chris@752
|
79
|
Chris@752
|
80 QString otherPath = other->getLocalFilename();
|
Chris@752
|
81 if (otherPath == "") {
|
Chris@752
|
82 otherPath = FileSource(other->getLocation()).getLocalFilename();
|
Chris@752
|
83 }
|
Chris@752
|
84
|
Chris@752
|
85 if (refPath == "" || otherPath == "") {
|
Chris@761
|
86 emit failed(m_toAlign,
|
Chris@761
|
87 tr("Failed to find local filepath for wave-file model"));
|
Chris@761
|
88 return;
|
Chris@752
|
89 }
|
Chris@752
|
90
|
Chris@752
|
91 auto alignmentModel =
|
Chris@752
|
92 std::make_shared<AlignmentModel>(m_reference, m_toAlign, ModelId());
|
Chris@752
|
93
|
Chris@752
|
94 m_alignmentModel = ModelById::add(alignmentModel);
|
Chris@752
|
95 other->setAlignment(m_alignmentModel);
|
Chris@752
|
96
|
Chris@752
|
97 m_process = new QProcess;
|
Chris@752
|
98 m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
|
Chris@752
|
99
|
Chris@752
|
100 connect(m_process,
|
Chris@752
|
101 SIGNAL(finished(int, QProcess::ExitStatus)),
|
Chris@752
|
102 this,
|
Chris@752
|
103 SLOT(programFinished(int, QProcess::ExitStatus)));
|
Chris@752
|
104
|
Chris@752
|
105 QStringList args;
|
Chris@752
|
106 args << refPath << otherPath;
|
Chris@752
|
107
|
Chris@752
|
108 SVCERR << "ExternalProgramAligner: Starting program \""
|
Chris@752
|
109 << m_program << "\" with args: ";
|
Chris@752
|
110 for (auto a: args) {
|
Chris@752
|
111 SVCERR << "\"" << a << "\" ";
|
Chris@752
|
112 }
|
Chris@752
|
113 SVCERR << endl;
|
Chris@752
|
114
|
Chris@752
|
115 m_process->start(m_program, args);
|
Chris@752
|
116
|
Chris@752
|
117 bool success = m_process->waitForStarted();
|
Chris@752
|
118
|
Chris@752
|
119 if (!success) {
|
Chris@752
|
120
|
Chris@752
|
121 SVCERR << "ERROR: ExternalProgramAligner: Program did not start" << endl;
|
Chris@769
|
122
|
Chris@769
|
123 other->setAlignment({});
|
Chris@769
|
124 ModelById::release(m_alignmentModel);
|
Chris@769
|
125 delete m_process;
|
Chris@769
|
126 m_process = nullptr;
|
Chris@769
|
127
|
Chris@761
|
128 emit failed(m_toAlign,
|
Chris@761
|
129 tr("Alignment program \"%1\" did not start")
|
Chris@761
|
130 .arg(m_program));
|
Chris@752
|
131
|
Chris@752
|
132 } else {
|
Chris@769
|
133 alignmentModel->setCompletion(10);
|
Chris@752
|
134 m_document->addNonDerivedModel(m_alignmentModel);
|
Chris@752
|
135 }
|
Chris@752
|
136 }
|
Chris@752
|
137
|
Chris@752
|
138 void
|
Chris@769
|
139 ExternalProgramAligner::programFinished(int exitCode,
|
Chris@769
|
140 QProcess::ExitStatus status)
|
Chris@752
|
141 {
|
Chris@752
|
142 SVCERR << "ExternalProgramAligner::programFinished" << endl;
|
Chris@752
|
143
|
Chris@752
|
144 QProcess *process = qobject_cast<QProcess *>(sender());
|
Chris@752
|
145
|
Chris@752
|
146 if (process != m_process) {
|
Chris@752
|
147 SVCERR << "ERROR: ExternalProgramAligner: Emitting process " << process
|
Chris@752
|
148 << " is not my process!" << endl;
|
Chris@752
|
149 return;
|
Chris@752
|
150 }
|
Chris@752
|
151
|
Chris@752
|
152 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
|
Chris@752
|
153 if (!alignmentModel) {
|
Chris@752
|
154 SVCERR << "ExternalProgramAligner: AlignmentModel no longer exists"
|
Chris@752
|
155 << endl;
|
Chris@752
|
156 return;
|
Chris@752
|
157 }
|
Chris@769
|
158
|
Chris@769
|
159 QString errorText;
|
Chris@752
|
160
|
Chris@752
|
161 if (exitCode == 0 && status == 0) {
|
Chris@752
|
162
|
Chris@752
|
163 CSVFormat format;
|
Chris@752
|
164 format.setModelType(CSVFormat::TwoDimensionalModel);
|
Chris@752
|
165 format.setTimingType(CSVFormat::ExplicitTiming);
|
Chris@752
|
166 format.setTimeUnits(CSVFormat::TimeSeconds);
|
Chris@752
|
167 format.setColumnCount(2);
|
Chris@752
|
168 // The output format has time in the reference file first, and
|
Chris@752
|
169 // time in the "other" file in the second column. This is a
|
Chris@752
|
170 // more natural approach for a command-line alignment tool,
|
Chris@752
|
171 // but it's the opposite of what we expect for native
|
Chris@752
|
172 // alignment paths, which map from "other" file to
|
Chris@752
|
173 // reference. These column purpose settings reflect that.
|
Chris@752
|
174 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
|
Chris@752
|
175 format.setColumnPurpose(0, CSVFormat::ColumnValue);
|
Chris@752
|
176 format.setAllowQuoting(false);
|
Chris@752
|
177 format.setSeparator(',');
|
Chris@752
|
178
|
Chris@752
|
179 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
|
Chris@752
|
180 if (!reader.isOK()) {
|
Chris@752
|
181 SVCERR << "ERROR: ExternalProgramAligner: Failed to parse output"
|
Chris@752
|
182 << endl;
|
Chris@769
|
183 errorText = tr("Failed to parse output of program: %1")
|
Chris@761
|
184 .arg(reader.getError());
|
Chris@769
|
185 alignmentModel->setError(errorText);
|
Chris@752
|
186 goto done;
|
Chris@752
|
187 }
|
Chris@752
|
188
|
Chris@752
|
189 //!!! to use ById?
|
Chris@752
|
190
|
Chris@752
|
191 Model *csvOutput = reader.load();
|
Chris@752
|
192
|
Chris@752
|
193 SparseTimeValueModel *path =
|
Chris@752
|
194 qobject_cast<SparseTimeValueModel *>(csvOutput);
|
Chris@752
|
195 if (!path) {
|
Chris@752
|
196 SVCERR << "ERROR: ExternalProgramAligner: Output did not convert to sparse time-value model"
|
Chris@752
|
197 << endl;
|
Chris@769
|
198 errorText =
|
Chris@761
|
199 tr("Output of alignment program was not in the proper format");
|
Chris@769
|
200 alignmentModel->setError(errorText);
|
Chris@752
|
201 delete csvOutput;
|
Chris@752
|
202 goto done;
|
Chris@752
|
203 }
|
Chris@752
|
204
|
Chris@752
|
205 if (path->isEmpty()) {
|
Chris@752
|
206 SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings"
|
Chris@752
|
207 << endl;
|
Chris@769
|
208 errorText =
|
Chris@761
|
209 tr("Output of alignment program contained no mappings");
|
Chris@769
|
210 alignmentModel->setError(errorText);
|
Chris@752
|
211 delete path;
|
Chris@752
|
212 goto done;
|
Chris@752
|
213 }
|
Chris@752
|
214
|
Chris@752
|
215 SVCERR << "ExternalProgramAligner: Setting alignment path ("
|
Chris@752
|
216 << path->getEventCount() << " point(s))" << endl;
|
Chris@752
|
217
|
Chris@752
|
218 auto pathId =
|
Chris@752
|
219 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
|
Chris@752
|
220 alignmentModel->setPathFrom(pathId);
|
Chris@769
|
221 alignmentModel->setCompletion(100);
|
Chris@752
|
222
|
Chris@752
|
223 ModelById::release(pathId);
|
Chris@752
|
224
|
Chris@752
|
225 } else {
|
Chris@752
|
226 SVCERR << "ERROR: ExternalProgramAligner: Aligner program "
|
Chris@752
|
227 << "failed: exit code " << exitCode << ", status " << status
|
Chris@752
|
228 << endl;
|
Chris@769
|
229 errorText = tr("Aligner process returned non-zero exit status");
|
Chris@769
|
230 alignmentModel->setError(errorText);
|
Chris@752
|
231 }
|
Chris@752
|
232
|
Chris@752
|
233 done:
|
Chris@752
|
234 delete m_process;
|
Chris@752
|
235 m_process = nullptr;
|
Chris@769
|
236
|
Chris@769
|
237 // "This should be emitted as the last thing the aligner does, as
|
Chris@769
|
238 // the recipient may delete the aligner during the call."
|
Chris@769
|
239 if (errorText == "") {
|
Chris@769
|
240 emit complete(m_alignmentModel);
|
Chris@769
|
241 } else {
|
Chris@769
|
242 emit failed(m_toAlign, errorText);
|
Chris@769
|
243 }
|
Chris@752
|
244 }
|