annotate framework/Align.cpp @ 530:066d8c5f3b21 project-file-rework

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