annotate align/ExternalProgramAligner.cpp @ 768:1b1960009be6 pitch-align

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