annotate align/ExternalProgramAligner.cpp @ 769:a316cb6fed81 pitch-align

Fixes to notification and completion in aligners
author Chris Cannam
date Thu, 28 May 2020 17:04:36 +0100
parents 6429a164b7e1
children 699b5b130ea2
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@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 }