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@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 Chris@422: #include Chris@430: #include 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: (ref); Chris@420: Chris@420: RangeSummarisableTimeValueModel *rm = qobject_cast Chris@420: (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 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(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(ref); Chris@420: WaveFileModel *rm = qobject_cast(other); Chris@420: Chris@420: if (!rm) return false; // but this should have been tested already 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@420: QString refPath = reference->getLocalFilename(); Chris@420: QString otherPath = rm->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(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(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: