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