comparison align/ExternalProgramAligner.cpp @ 763:da57ab54f0e8

Merge from branch pitch-align. Doesn't actually do pitch alignment here, but this is the groundwork.
author Chris Cannam
date Wed, 13 May 2020 14:10:47 +0100
parents 6429a164b7e1
children a316cb6fed81
comparison
equal deleted inserted replaced
760:3a63f1f61bd6 763:da57ab54f0e8
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2
3 /*
4 Sonic Visualiser
5 An audio file viewer and annotation editor.
6 Centre for Digital Music, Queen Mary, University of London.
7
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License as
10 published by the Free Software Foundation; either version 2 of the
11 License, or (at your option) any later version. See the file
12 COPYING included with this distribution for more information.
13 */
14
15 #include "ExternalProgramAligner.h"
16
17 #include <QFileInfo>
18 #include <QApplication>
19
20 #include "data/model/ReadOnlyWaveFileModel.h"
21 #include "data/model/SparseTimeValueModel.h"
22 #include "data/model/AlignmentModel.h"
23
24 #include "data/fileio/CSVFileReader.h"
25
26 #include "framework/Document.h"
27
28 ExternalProgramAligner::ExternalProgramAligner(Document *doc,
29 ModelId reference,
30 ModelId toAlign,
31 QString program) :
32 m_document(doc),
33 m_reference(reference),
34 m_toAlign(toAlign),
35 m_program(program),
36 m_process(nullptr)
37 {
38 }
39
40 ExternalProgramAligner::~ExternalProgramAligner()
41 {
42 delete m_process;
43 }
44
45 bool
46 ExternalProgramAligner::isAvailable(QString program)
47 {
48 QFileInfo file(program);
49 return file.exists() && file.isExecutable();
50 }
51
52 void
53 ExternalProgramAligner::begin()
54 {
55 // Run an external program, passing to it paths to the main
56 // model's audio file and the new model's audio file. It returns
57 // the path in CSV form through stdout.
58
59 auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(m_reference);
60 auto other = ModelById::getAs<ReadOnlyWaveFileModel>(m_toAlign);
61 if (!reference || !other) {
62 SVCERR << "ERROR: ExternalProgramAligner: Can't align non-read-only models via program (no local filename available)" << endl;
63 return;
64 }
65
66 while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
67 qApp->processEvents();
68 }
69
70 QString refPath = reference->getLocalFilename();
71 if (refPath == "") {
72 refPath = FileSource(reference->getLocation()).getLocalFilename();
73 }
74
75 QString otherPath = other->getLocalFilename();
76 if (otherPath == "") {
77 otherPath = FileSource(other->getLocation()).getLocalFilename();
78 }
79
80 if (refPath == "" || otherPath == "") {
81 emit failed(m_toAlign,
82 tr("Failed to find local filepath for wave-file model"));
83 return;
84 }
85
86 auto alignmentModel =
87 std::make_shared<AlignmentModel>(m_reference, m_toAlign, ModelId());
88
89 m_alignmentModel = ModelById::add(alignmentModel);
90 other->setAlignment(m_alignmentModel);
91
92 m_process = new QProcess;
93 m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
94
95 connect(m_process,
96 SIGNAL(finished(int, QProcess::ExitStatus)),
97 this,
98 SLOT(programFinished(int, QProcess::ExitStatus)));
99
100 QStringList args;
101 args << refPath << otherPath;
102
103 SVCERR << "ExternalProgramAligner: Starting program \""
104 << m_program << "\" with args: ";
105 for (auto a: args) {
106 SVCERR << "\"" << a << "\" ";
107 }
108 SVCERR << endl;
109
110 m_process->start(m_program, args);
111
112 bool success = m_process->waitForStarted();
113
114 if (!success) {
115
116 SVCERR << "ERROR: ExternalProgramAligner: Program did not start" << endl;
117 emit failed(m_toAlign,
118 tr("Alignment program \"%1\" did not start")
119 .arg(m_program));
120
121 other->setAlignment({});
122 ModelById::release(m_alignmentModel);
123 delete m_process;
124 m_process = nullptr;
125
126 } else {
127 m_document->addNonDerivedModel(m_alignmentModel);
128 }
129 }
130
131 void
132 ExternalProgramAligner::programFinished(int exitCode, QProcess::ExitStatus status)
133 {
134 SVCERR << "ExternalProgramAligner::programFinished" << endl;
135
136 QProcess *process = qobject_cast<QProcess *>(sender());
137
138 if (process != m_process) {
139 SVCERR << "ERROR: ExternalProgramAligner: Emitting process " << process
140 << " is not my process!" << endl;
141 return;
142 }
143
144 auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
145 if (!alignmentModel) {
146 SVCERR << "ExternalProgramAligner: AlignmentModel no longer exists"
147 << endl;
148 return;
149 }
150
151 if (exitCode == 0 && status == 0) {
152
153 CSVFormat format;
154 format.setModelType(CSVFormat::TwoDimensionalModel);
155 format.setTimingType(CSVFormat::ExplicitTiming);
156 format.setTimeUnits(CSVFormat::TimeSeconds);
157 format.setColumnCount(2);
158 // The output format has time in the reference file first, and
159 // time in the "other" file in the second column. This is a
160 // more natural approach for a command-line alignment tool,
161 // but it's the opposite of what we expect for native
162 // alignment paths, which map from "other" file to
163 // reference. These column purpose settings reflect that.
164 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
165 format.setColumnPurpose(0, CSVFormat::ColumnValue);
166 format.setAllowQuoting(false);
167 format.setSeparator(',');
168
169 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
170 if (!reader.isOK()) {
171 SVCERR << "ERROR: ExternalProgramAligner: Failed to parse output"
172 << endl;
173 QString error = tr("Failed to parse output of program: %1")
174 .arg(reader.getError());
175 alignmentModel->setError(error);
176 emit failed(m_toAlign, error);
177 goto done;
178 }
179
180 //!!! to use ById?
181
182 Model *csvOutput = reader.load();
183
184 SparseTimeValueModel *path =
185 qobject_cast<SparseTimeValueModel *>(csvOutput);
186 if (!path) {
187 SVCERR << "ERROR: ExternalProgramAligner: Output did not convert to sparse time-value model"
188 << endl;
189 QString error =
190 tr("Output of alignment program was not in the proper format");
191 alignmentModel->setError(error);
192 delete csvOutput;
193 emit failed(m_toAlign, error);
194 goto done;
195 }
196
197 if (path->isEmpty()) {
198 SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings"
199 << endl;
200 QString error =
201 tr("Output of alignment program contained no mappings");
202 alignmentModel->setError(error);
203 delete path;
204 emit failed(m_toAlign, error);
205 goto done;
206 }
207
208 SVCERR << "ExternalProgramAligner: Setting alignment path ("
209 << path->getEventCount() << " point(s))" << endl;
210
211 auto pathId =
212 ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
213 alignmentModel->setPathFrom(pathId);
214
215 emit complete(m_alignmentModel);
216
217 ModelById::release(pathId);
218
219 } else {
220 SVCERR << "ERROR: ExternalProgramAligner: Aligner program "
221 << "failed: exit code " << exitCode << ", status " << status
222 << endl;
223 QString error = tr("Aligner process returned non-zero exit status");
224 alignmentModel->setError(error);
225 emit failed(m_toAlign, error);
226 }
227
228 done:
229 delete m_process;
230 m_process = nullptr;
231 }