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