Chris@420: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */
Chris@420: 
Chris@420: /*
Chris@420:     Sonic Visualiser
Chris@420:     An audio file viewer and annotation editor.
Chris@420:     Centre for Digital Music, Queen Mary, University of London.
Chris@420:     This file copyright 2006 Chris Cannam and QMUL.
Chris@420:     
Chris@420:     This program is free software; you can redistribute it and/or
Chris@420:     modify it under the terms of the GNU General Public License as
Chris@420:     published by the Free Software Foundation; either version 2 of the
Chris@420:     License, or (at your option) any later version.  See the file
Chris@420:     COPYING included with this distribution for more information.
Chris@420: */
Chris@420: 
Chris@420: #include "Align.h"
Chris@420: 
Chris@420: #include "data/model/WaveFileModel.h"
Chris@515: #include "data/model/ReadOnlyWaveFileModel.h"
Chris@420: #include "data/model/AggregateWaveModel.h"
Chris@420: #include "data/model/RangeSummarisableTimeValueModel.h"
Chris@420: #include "data/model/SparseTimeValueModel.h"
Chris@420: #include "data/model/AlignmentModel.h"
Chris@420: 
Chris@420: #include "data/fileio/CSVFileReader.h"
Chris@420: 
Chris@420: #include "transform/TransformFactory.h"
Chris@420: #include "transform/ModelTransformerFactory.h"
Chris@420: #include "transform/FeatureExtractionModelTransformer.h"
Chris@420: 
Chris@420: #include <QProcess>
Chris@422: #include <QSettings>
Chris@430: #include <QApplication>
Chris@422: 
Chris@422: bool
Chris@422: Align::alignModel(Model *ref, Model *other)
Chris@422: {
Chris@422:     QSettings settings;
Chris@422:     settings.beginGroup("Preferences");
Chris@422:     bool useProgram = settings.value("use-external-alignment", false).toBool();
Chris@422:     QString program = settings.value("external-alignment-program", "").toString();
Chris@422:     settings.endGroup();
Chris@422: 
Chris@422:     if (useProgram && (program != "")) {
Chris@422:         return alignModelViaProgram(ref, other, program);
Chris@422:     } else {
Chris@422:         return alignModelViaTransform(ref, other);
Chris@422:     }
Chris@422: }
Chris@420: 
Chris@428: QString
Chris@428: Align::getAlignmentTransformName()
Chris@428: {
Chris@428:     QSettings settings;
Chris@428:     settings.beginGroup("Alignment");
Chris@428:     TransformId id =
Chris@428:         settings.value("transform-id",
Chris@428:                        "vamp:match-vamp-plugin:match:path").toString();
Chris@428:     settings.endGroup();
Chris@428:     return id;
Chris@428: }
Chris@428: 
Chris@428: bool
Chris@428: Align::canAlign() 
Chris@428: {
Chris@428:     TransformId id = getAlignmentTransformName();
Chris@428:     TransformFactory *factory = TransformFactory::getInstance();
Chris@428:     return factory->haveTransform(id);
Chris@428: }
Chris@428: 
Chris@420: bool
Chris@420: Align::alignModelViaTransform(Model *ref, Model *other)
Chris@420: {
Chris@420:     RangeSummarisableTimeValueModel *reference = qobject_cast
Chris@420:         <RangeSummarisableTimeValueModel *>(ref);
Chris@420:     
Chris@420:     RangeSummarisableTimeValueModel *rm = qobject_cast
Chris@420:         <RangeSummarisableTimeValueModel *>(other);
Chris@420: 
Chris@420:     if (!reference || !rm) return false; // but this should have been tested already
Chris@420:    
Chris@420:     // This involves creating three new models:
Chris@420: 
Chris@420:     // 1. an AggregateWaveModel to provide the mixdowns of the main
Chris@420:     // model and the new model in its two channels, as input to the
Chris@420:     // MATCH plugin
Chris@420: 
Chris@420:     // 2. a SparseTimeValueModel, which is the model automatically
Chris@420:     // created by FeatureExtractionPluginTransformer when running the
Chris@420:     // MATCH plugin (thus containing the alignment path)
Chris@420: 
Chris@420:     // 3. an AlignmentModel, which stores the path model and carries
Chris@420:     // out alignment lookups on it.
Chris@420: 
Chris@420:     // The first two of these are provided as arguments to the
Chris@420:     // constructor for the third, which takes responsibility for
Chris@420:     // deleting them.  The AlignmentModel, meanwhile, is passed to the
Chris@420:     // new model we are aligning, which also takes responsibility for
Chris@420:     // it.  We should not have to delete any of these new models here.
Chris@420: 
Chris@420:     AggregateWaveModel::ChannelSpecList components;
Chris@420: 
Chris@420:     components.push_back(AggregateWaveModel::ModelChannelSpec
Chris@420:                          (reference, -1));
Chris@420: 
Chris@420:     components.push_back(AggregateWaveModel::ModelChannelSpec
Chris@420:                          (rm, -1));
Chris@420: 
Chris@420:     Model *aggregateModel = new AggregateWaveModel(components);
Chris@420:     ModelTransformer::Input aggregate(aggregateModel);
Chris@420: 
Chris@428:     TransformId id = getAlignmentTransformName();
Chris@420:     
Chris@420:     TransformFactory *tf = TransformFactory::getInstance();
Chris@420: 
Chris@420:     Transform transform = tf->getDefaultTransformFor
Chris@420:         (id, aggregateModel->getSampleRate());
Chris@420: 
Chris@420:     transform.setStepSize(transform.getBlockSize()/2);
Chris@420:     transform.setParameter("serialise", 1);
Chris@420:     transform.setParameter("smooth", 0);
Chris@420: 
Chris@420:     SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl;
Chris@420: 
Chris@420:     ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance();
Chris@420: 
Chris@420:     QString message;
Chris@420:     Model *transformOutput = mtf->transform(transform, aggregate, message);
Chris@420: 
Chris@420:     if (!transformOutput) {
Chris@420:         transform.setStepSize(0);
Chris@420:         transformOutput = mtf->transform(transform, aggregate, message);
Chris@420:     }
Chris@420: 
Chris@420:     SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *>
Chris@420:         (transformOutput);
Chris@420: 
Chris@420:     if (!path) {
Chris@420:         cerr << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl;
Chris@420:         delete transformOutput;
Chris@420:         delete aggregateModel;
Chris@420: 	m_error = message;
Chris@420:         return false;
Chris@420:     }
Chris@420: 
Chris@420:     path->setCompletion(0);
Chris@420: 
Chris@420:     AlignmentModel *alignmentModel = new AlignmentModel
Chris@420:         (reference, other, aggregateModel, path);
Chris@420: 
Chris@428:     connect(alignmentModel, SIGNAL(completionChanged()),
Chris@428:             this, SLOT(alignmentCompletionChanged()));
Chris@428:     
Chris@420:     rm->setAlignment(alignmentModel);
Chris@420: 
Chris@420:     return true;
Chris@420: }
Chris@420: 
Chris@428: void
Chris@428: Align::alignmentCompletionChanged()
Chris@428: {
Chris@428:     AlignmentModel *am = qobject_cast<AlignmentModel *>(sender());
Chris@428:     if (!am) return;
Chris@428:     if (am->isReady()) {
Chris@428:         disconnect(am, SIGNAL(completionChanged()),
Chris@428:                    this, SLOT(alignmentCompletionChanged()));
Chris@428:         emit alignmentComplete(am);
Chris@428:     }
Chris@428: }
Chris@428: 
Chris@420: bool
Chris@422: Align::alignModelViaProgram(Model *ref, Model *other, QString program)
Chris@420: {
Chris@420:     WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref);
Chris@420:     WaveFileModel *rm = qobject_cast<WaveFileModel *>(other);
Chris@420: 
Chris@515:     if (!reference || !rm) {
Chris@515:         return false; // but this should have been tested already
Chris@515:     }
Chris@420: 
Chris@430:     while (!reference->isReady(0) || !rm->isReady(0)) {
Chris@430:         qApp->processEvents();
Chris@430:     }
Chris@430:     
Chris@420:     // Run an external program, passing to it paths to the main
Chris@420:     // model's audio file and the new model's audio file. It returns
Chris@420:     // the path in CSV form through stdout.
Chris@420: 
Chris@515:     ReadOnlyWaveFileModel *roref = qobject_cast<ReadOnlyWaveFileModel *>(reference);
Chris@515:     ReadOnlyWaveFileModel *rorm = qobject_cast<ReadOnlyWaveFileModel *>(rm);
Chris@515:     if (!roref || !rorm) {
Chris@515:         cerr << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl;
Chris@515:         return false;
Chris@515:     }
Chris@515:     
Chris@515:     QString refPath = roref->getLocalFilename();
Chris@515:     QString otherPath = rorm->getLocalFilename();
Chris@420: 
Chris@420:     if (refPath == "" || otherPath == "") {
Chris@420: 	m_error = "Failed to find local filepath for wave-file model";
Chris@420: 	return false;
Chris@420:     }
Chris@420: 
Chris@423:     m_error = "";
Chris@423:     
Chris@423:     AlignmentModel *alignmentModel = new AlignmentModel(reference, other, 0, 0);
Chris@423:     rm->setAlignment(alignmentModel);
Chris@423: 
Chris@423:     QProcess *process = new QProcess;
Chris@420:     QStringList args;
Chris@420:     args << refPath << otherPath;
Chris@423:     
Chris@423:     connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
Chris@423:             this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus)));
Chris@420: 
Chris@423:     m_processModels[process] = alignmentModel;
Chris@423:     process->start(program, args);
Chris@420: 
Chris@423:     bool success = process->waitForStarted();
Chris@423: 
Chris@423:     if (!success) {
Chris@423:         cerr << "ERROR: Align::alignModelViaProgram: Program did not start"
Chris@423:              << endl;
Chris@423:         m_error = "Alignment program could not be started";
Chris@423:         m_processModels.erase(process);
Chris@424:         rm->setAlignment(0); // deletes alignmentModel as well
Chris@423:         delete process;
Chris@423:     }
Chris@423: 
Chris@423:     return success;
Chris@423: }
Chris@423: 
Chris@423: void
Chris@423: Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status)
Chris@423: {
Chris@423:     cerr << "Align::alignmentProgramFinished" << endl;
Chris@423:     
Chris@423:     QProcess *process = qobject_cast<QProcess *>(sender());
Chris@423: 
Chris@423:     if (m_processModels.find(process) == m_processModels.end()) {
Chris@423:         cerr << "ERROR: Align::alignmentProgramFinished: Process " << process
Chris@423:              << " not found in process model map!" << endl;
Chris@423:         return;
Chris@423:     }
Chris@423: 
Chris@423:     AlignmentModel *alignmentModel = m_processModels[process];
Chris@423:     
Chris@423:     if (exitCode == 0 && status == 0) {
Chris@420: 
Chris@420: 	CSVFormat format;
Chris@420: 	format.setModelType(CSVFormat::TwoDimensionalModel);
Chris@420: 	format.setTimingType(CSVFormat::ExplicitTiming);
Chris@420: 	format.setTimeUnits(CSVFormat::TimeSeconds);
Chris@420: 	format.setColumnCount(2);
Chris@425:         // The output format has time in the reference file first, and
Chris@425:         // time in the "other" file in the second column. This is a
Chris@425:         // more natural approach for a command-line alignment tool,
Chris@425:         // but it's the opposite of what we expect for native
Chris@425:         // alignment paths, which map from "other" file to
Chris@425:         // reference. These column purpose settings reflect that.
Chris@425: 	format.setColumnPurpose(1, CSVFormat::ColumnStartTime);
Chris@425: 	format.setColumnPurpose(0, CSVFormat::ColumnValue);
Chris@420: 	format.setAllowQuoting(false);
Chris@420: 	format.setSeparator(',');
Chris@420: 
Chris@423: 	CSVFileReader reader(process, format, alignmentModel->getSampleRate());
Chris@420: 	if (!reader.isOK()) {
Chris@423:             cerr << "ERROR: Align::alignmentProgramFinished: Failed to parse output"
Chris@423:                  << endl;
Chris@420: 	    m_error = QString("Failed to parse output of program: %1")
Chris@420: 		.arg(reader.getError());
Chris@423:             goto done;
Chris@420: 	}
Chris@420: 
Chris@420: 	Model *csvOutput = reader.load();
Chris@420: 
Chris@420: 	SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput);
Chris@420: 	if (!path) {
Chris@423:             cerr << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model"
Chris@423:                  << endl;
Chris@420: 	    m_error = QString("Output of program did not produce sparse time-value model");
Chris@423:             goto done;
Chris@420: 	}
Chris@420: 
Chris@420: 	if (path->getPoints().empty()) {
Chris@423:             cerr << "ERROR: Align::alignmentProgramFinished: Output contained no mappings"
Chris@423:                  << endl;
Chris@420: 	    m_error = QString("Output of alignment program contained no mappings");
Chris@423:             goto done;
Chris@420: 	}
Chris@420: 
Chris@423:         cerr << "Align::alignmentProgramFinished: Setting alignment path ("
Chris@423:              << path->getPoints().size() << " point(s))" << endl;
Chris@423:         
Chris@423:         alignmentModel->setPathFrom(path);
Chris@420: 
Chris@428:         emit alignmentComplete(alignmentModel);
Chris@428:         
Chris@420:     } else {
Chris@423:         cerr << "ERROR: Align::alignmentProgramFinished: Aligner program "
Chris@423:              << "failed: exit code " << exitCode << ", status " << status
Chris@423:              << endl;
Chris@420: 	m_error = "Aligner process returned non-zero exit status";
Chris@420:     }
Chris@420: 
Chris@423: done:
Chris@423:     m_processModels.erase(process);
Chris@423:     delete process;
Chris@420: }
Chris@420: