annotate framework/Align.cpp @ 665:e19c609a7bec

Update so Document owns the alignment model's input aggregate model
author Chris Cannam
date Thu, 04 Apr 2019 16:17:11 +0100
parents e2715204feaa
children 21673429dba5
rev   line source
Chris@420 1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
Chris@420 2
Chris@420 3 /*
Chris@420 4 Sonic Visualiser
Chris@420 5 An audio file viewer and annotation editor.
Chris@420 6 Centre for Digital Music, Queen Mary, University of London.
Chris@420 7 This file copyright 2006 Chris Cannam and QMUL.
Chris@420 8
Chris@420 9 This program is free software; you can redistribute it and/or
Chris@420 10 modify it under the terms of the GNU General Public License as
Chris@420 11 published by the Free Software Foundation; either version 2 of the
Chris@420 12 License, or (at your option) any later version. See the file
Chris@420 13 COPYING included with this distribution for more information.
Chris@420 14 */
Chris@420 15
Chris@420 16 #include "Align.h"
Chris@665 17 #include "Document.h"
Chris@420 18
Chris@420 19 #include "data/model/WaveFileModel.h"
Chris@515 20 #include "data/model/ReadOnlyWaveFileModel.h"
Chris@420 21 #include "data/model/AggregateWaveModel.h"
Chris@420 22 #include "data/model/RangeSummarisableTimeValueModel.h"
Chris@420 23 #include "data/model/SparseTimeValueModel.h"
Chris@420 24 #include "data/model/AlignmentModel.h"
Chris@420 25
Chris@420 26 #include "data/fileio/CSVFileReader.h"
Chris@420 27
Chris@420 28 #include "transform/TransformFactory.h"
Chris@420 29 #include "transform/ModelTransformerFactory.h"
Chris@420 30 #include "transform/FeatureExtractionModelTransformer.h"
Chris@420 31
Chris@420 32 #include <QProcess>
Chris@422 33 #include <QSettings>
Chris@430 34 #include <QApplication>
Chris@422 35
Chris@422 36 bool
Chris@665 37 Align::alignModel(Document *doc, Model *ref, Model *other)
Chris@422 38 {
Chris@422 39 QSettings settings;
Chris@422 40 settings.beginGroup("Preferences");
Chris@422 41 bool useProgram = settings.value("use-external-alignment", false).toBool();
Chris@422 42 QString program = settings.value("external-alignment-program", "").toString();
Chris@422 43 settings.endGroup();
Chris@422 44
Chris@422 45 if (useProgram && (program != "")) {
Chris@665 46 return alignModelViaProgram(doc, ref, other, program);
Chris@422 47 } else {
Chris@665 48 return alignModelViaTransform(doc, ref, other);
Chris@422 49 }
Chris@422 50 }
Chris@420 51
Chris@428 52 QString
Chris@428 53 Align::getAlignmentTransformName()
Chris@428 54 {
Chris@428 55 QSettings settings;
Chris@428 56 settings.beginGroup("Alignment");
Chris@428 57 TransformId id =
Chris@428 58 settings.value("transform-id",
Chris@428 59 "vamp:match-vamp-plugin:match:path").toString();
Chris@428 60 settings.endGroup();
Chris@428 61 return id;
Chris@428 62 }
Chris@428 63
Chris@428 64 bool
Chris@428 65 Align::canAlign()
Chris@428 66 {
Chris@428 67 TransformId id = getAlignmentTransformName();
Chris@428 68 TransformFactory *factory = TransformFactory::getInstance();
Chris@428 69 return factory->haveTransform(id);
Chris@428 70 }
Chris@428 71
Chris@420 72 bool
Chris@665 73 Align::alignModelViaTransform(Document *doc, Model *ref, Model *other)
Chris@420 74 {
Chris@420 75 RangeSummarisableTimeValueModel *reference = qobject_cast
Chris@420 76 <RangeSummarisableTimeValueModel *>(ref);
Chris@420 77
Chris@420 78 RangeSummarisableTimeValueModel *rm = qobject_cast
Chris@420 79 <RangeSummarisableTimeValueModel *>(other);
Chris@420 80
Chris@420 81 if (!reference || !rm) return false; // but this should have been tested already
Chris@420 82
Chris@420 83 // This involves creating three new models:
Chris@420 84
Chris@420 85 // 1. an AggregateWaveModel to provide the mixdowns of the main
Chris@420 86 // model and the new model in its two channels, as input to the
Chris@420 87 // MATCH plugin
Chris@420 88
Chris@420 89 // 2. a SparseTimeValueModel, which is the model automatically
Chris@420 90 // created by FeatureExtractionPluginTransformer when running the
Chris@420 91 // MATCH plugin (thus containing the alignment path)
Chris@420 92
Chris@420 93 // 3. an AlignmentModel, which stores the path model and carries
Chris@420 94 // out alignment lookups on it.
Chris@420 95
Chris@420 96 // The first two of these are provided as arguments to the
Chris@420 97 // constructor for the third, which takes responsibility for
Chris@420 98 // deleting them. The AlignmentModel, meanwhile, is passed to the
Chris@420 99 // new model we are aligning, which also takes responsibility for
Chris@420 100 // it. We should not have to delete any of these new models here.
Chris@420 101
Chris@420 102 AggregateWaveModel::ChannelSpecList components;
Chris@420 103
Chris@420 104 components.push_back(AggregateWaveModel::ModelChannelSpec
Chris@420 105 (reference, -1));
Chris@420 106
Chris@420 107 components.push_back(AggregateWaveModel::ModelChannelSpec
Chris@420 108 (rm, -1));
Chris@420 109
Chris@665 110 AggregateWaveModel *aggregateModel = new AggregateWaveModel(components);
Chris@665 111 doc->addAggregateModel(aggregateModel);
Chris@665 112
Chris@420 113 ModelTransformer::Input aggregate(aggregateModel);
Chris@420 114
Chris@428 115 TransformId id = getAlignmentTransformName();
Chris@420 116
Chris@420 117 TransformFactory *tf = TransformFactory::getInstance();
Chris@420 118
Chris@420 119 Transform transform = tf->getDefaultTransformFor
Chris@420 120 (id, aggregateModel->getSampleRate());
Chris@420 121
Chris@420 122 transform.setStepSize(transform.getBlockSize()/2);
Chris@420 123 transform.setParameter("serialise", 1);
Chris@420 124 transform.setParameter("smooth", 0);
Chris@420 125
Chris@420 126 SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
Chris@420 127
Chris@420 128 ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
Chris@420 129
Chris@420 130 QString message;
Chris@420 131 Model *transformOutput = mtf->transform(transform, aggregate, message);
Chris@420 132
Chris@420 133 if (!transformOutput) {
Chris@420 134 transform.setStepSize(0);
Chris@420 135 transformOutput = mtf->transform(transform, aggregate, message);
Chris@420 136 }
Chris@420 137
Chris@420 138 SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *>
Chris@420 139 (transformOutput);
Chris@420 140
Chris@420 141 if (!path) {
Chris@420 142 cerr << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
Chris@420 143 delete transformOutput;
Chris@420 144 delete aggregateModel;
Chris@595 145 m_error = message;
Chris@420 146 return false;
Chris@420 147 }
Chris@420 148
Chris@420 149 path->setCompletion(0);
Chris@420 150
Chris@420 151 AlignmentModel *alignmentModel = new AlignmentModel
Chris@665 152 (reference, other, path);
Chris@420 153
Chris@428 154 connect(alignmentModel, SIGNAL(completionChanged()),
Chris@428 155 this, SLOT(alignmentCompletionChanged()));
Chris@428 156
Chris@420 157 rm->setAlignment(alignmentModel);
Chris@420 158
Chris@420 159 return true;
Chris@420 160 }
Chris@420 161
Chris@428 162 void
Chris@428 163 Align::alignmentCompletionChanged()
Chris@428 164 {
Chris@428 165 AlignmentModel *am = qobject_cast<AlignmentModel *>(sender());
Chris@428 166 if (!am) return;
Chris@428 167 if (am->isReady()) {
Chris@428 168 disconnect(am, SIGNAL(completionChanged()),
Chris@428 169 this, SLOT(alignmentCompletionChanged()));
Chris@428 170 emit alignmentComplete(am);
Chris@428 171 }
Chris@428 172 }
Chris@428 173
Chris@420 174 bool
Chris@665 175 Align::alignModelViaProgram(Document *, Model *ref, Model *other, QString program)
Chris@420 176 {
Chris@420 177 WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref);
Chris@420 178 WaveFileModel *rm = qobject_cast<WaveFileModel *>(other);
Chris@420 179
Chris@515 180 if (!reference || !rm) {
Chris@515 181 return false; // but this should have been tested already
Chris@515 182 }
Chris@420 183
Chris@636 184 while (!reference->isReady(nullptr) || !rm->isReady(nullptr)) {
Chris@430 185 qApp->processEvents();
Chris@430 186 }
Chris@430 187
Chris@420 188 // Run an external program, passing to it paths to the main
Chris@420 189 // model's audio file and the new model's audio file. It returns
Chris@420 190 // the path in CSV form through stdout.
Chris@420 191
Chris@515 192 ReadOnlyWaveFileModel *roref = qobject_cast<ReadOnlyWaveFileModel *>(reference);
Chris@515 193 ReadOnlyWaveFileModel *rorm = qobject_cast<ReadOnlyWaveFileModel *>(rm);
Chris@515 194 if (!roref || !rorm) {
Chris@515 195 cerr << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
Chris@515 196 return false;
Chris@515 197 }
Chris@515 198
Chris@515 199 QString refPath = roref->getLocalFilename();
Chris@515 200 QString otherPath = rorm->getLocalFilename();
Chris@420 201
Chris@420 202 if (refPath == "" || otherPath == "") {
Chris@595 203 m_error = "Failed to find local filepath for wave-file model";
Chris@595 204 return false;
Chris@420 205 }
Chris@420 206
Chris@423 207 m_error = "";
Chris@423 208
Chris@665 209 AlignmentModel *alignmentModel =
Chris@665 210 new AlignmentModel(reference, other, nullptr);
Chris@423 211 rm->setAlignment(alignmentModel);
Chris@423 212
Chris@423 213 QProcess *process = new QProcess;
Chris@420 214 QStringList args;
Chris@420 215 args << refPath << otherPath;
Chris@423 216
Chris@423 217 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
Chris@423 218 this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
Chris@420 219
Chris@423 220 m_processModels[process] = alignmentModel;
Chris@423 221 process->start(program, args);
Chris@420 222
Chris@423 223 bool success = process->waitForStarted();
Chris@423 224
Chris@423 225 if (!success) {
Chris@423 226 cerr << "ERROR: Align::alignModelViaProgram: Program did not start"
Chris@423 227 << endl;
Chris@423 228 m_error = "Alignment program could not be started";
Chris@423 229 m_processModels.erase(process);
Chris@636 230 rm->setAlignment(nullptr); // deletes alignmentModel as well
Chris@423 231 delete process;
Chris@423 232 }
Chris@423 233
Chris@423 234 return success;
Chris@423 235 }
Chris@423 236
Chris@423 237 void
Chris@423 238 Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
Chris@423 239 {
Chris@423 240 cerr << "Align::alignmentProgramFinished" << endl;
Chris@423 241
Chris@423 242 QProcess *process = qobject_cast<QProcess *>(sender());
Chris@423 243
Chris@423 244 if (m_processModels.find(process) == m_processModels.end()) {
Chris@423 245 cerr << "ERROR: Align::alignmentProgramFinished: Process " << process
Chris@423 246 << " not found in process model map!" << endl;
Chris@423 247 return;
Chris@423 248 }
Chris@423 249
Chris@423 250 AlignmentModel *alignmentModel = m_processModels[process];
Chris@423 251
Chris@423 252 if (exitCode == 0 && status == 0) {
Chris@420 253
Chris@595 254 CSVFormat format;
Chris@595 255 format.setModelType(CSVFormat::TwoDimensionalModel);
Chris@595 256 format.setTimingType(CSVFormat::ExplicitTiming);
Chris@595 257 format.setTimeUnits(CSVFormat::TimeSeconds);
Chris@595 258 format.setColumnCount(2);
Chris@425 259 // The output format has time in the reference file first, and
Chris@425 260 // time in the "other" file in the second column. This is a
Chris@425 261 // more natural approach for a command-line alignment tool,
Chris@425 262 // but it's the opposite of what we expect for native
Chris@425 263 // alignment paths, which map from "other" file to
Chris@425 264 // reference. These column purpose settings reflect that.
Chris@595 265 format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
Chris@595 266 format.setColumnPurpose(0, CSVFormat::ColumnValue);
Chris@595 267 format.setAllowQuoting(false);
Chris@595 268 format.setSeparator(',');
Chris@420 269
Chris@595 270 CSVFileReader reader(process, format, alignmentModel->getSampleRate());
Chris@595 271 if (!reader.isOK()) {
Chris@423 272 cerr << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
Chris@423 273 << endl;
Chris@595 274 m_error = QString("Failed to parse output of program: %1")
Chris@595 275 .arg(reader.getError());
Chris@423 276 goto done;
Chris@595 277 }
Chris@420 278
Chris@595 279 Model *csvOutput = reader.load();
Chris@420 280
Chris@595 281 SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput);
Chris@595 282 if (!path) {
Chris@423 283 cerr << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
Chris@423 284 << endl;
Chris@595 285 m_error = QString("Output of program did not produce sparse time-value model");
Chris@423 286 goto done;
Chris@595 287 }
Chris@420 288
Chris@595 289 if (path->getPoints().empty()) {
Chris@423 290 cerr << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
Chris@423 291 << endl;
Chris@595 292 m_error = QString("Output of alignment program contained no mappings");
Chris@423 293 goto done;
Chris@595 294 }
Chris@420 295
Chris@423 296 cerr << "Align::alignmentProgramFinished: Setting alignment path ("
Chris@423 297 << path->getPoints().size() << " point(s))" << endl;
Chris@423 298
Chris@423 299 alignmentModel->setPathFrom(path);
Chris@420 300
Chris@428 301 emit alignmentComplete(alignmentModel);
Chris@428 302
Chris@420 303 } else {
Chris@423 304 cerr << "ERROR: Align::alignmentProgramFinished: Aligner program "
Chris@423 305 << "failed: exit code " << exitCode << ", status " << status
Chris@423 306 << endl;
Chris@595 307 m_error = "Aligner process returned non-zero exit status";
Chris@420 308 }
Chris@420 309
Chris@423 310 done:
Chris@423 311 m_processModels.erase(process);
Chris@423 312 delete process;
Chris@420 313 }
Chris@420 314