annotate align/ExternalProgramAligner.cpp @ 778:83a7b10b7415

Merge from branch pitch-align
author Chris Cannam
date Fri, 26 Jun 2020 13:48:52 +0100
parents 699b5b130ea2
children 5de2b710cfae
rev   line source
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 }