annotate align/ExternalProgramAligner.cpp @ 771:1d6cca5a5621 pitch-align

Allow use of proper sparse models (i.e. retaining event time info) in alignment; use this to switch to note alignment, which is what we have most recently been doing in the external program. Not currently producing correct results, though
author Chris Cannam
date Fri, 29 May 2020 17:39:02 +0100
parents a316cb6fed81
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 }