Mercurial > hg > svapp
changeset 520:c3648c667a0b 3.0-integration
Merge from branch "alignment-simple"
author | Chris Cannam |
---|---|
date | Thu, 21 Apr 2016 15:06:43 +0100 |
parents | f7ec9e410108 (current diff) b926f08909b8 (diff) |
children | 169aa5203faa 1682dd81d0ef |
files | svapp.pro |
diffstat | 7 files changed, 498 insertions(+), 107 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/Align.cpp Thu Apr 21 15:06:43 2016 +0100 @@ -0,0 +1,310 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam and QMUL. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + +#include "Align.h" + +#include "data/model/WaveFileModel.h" +#include "data/model/ReadOnlyWaveFileModel.h" +#include "data/model/AggregateWaveModel.h" +#include "data/model/RangeSummarisableTimeValueModel.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/AlignmentModel.h" + +#include "data/fileio/CSVFileReader.h" + +#include "transform/TransformFactory.h" +#include "transform/ModelTransformerFactory.h" +#include "transform/FeatureExtractionModelTransformer.h" + +#include <QProcess> +#include <QSettings> +#include <QApplication> + +bool +Align::alignModel(Model *ref, Model *other) +{ + QSettings settings; + settings.beginGroup("Preferences"); + bool useProgram = settings.value("use-external-alignment", false).toBool(); + QString program = settings.value("external-alignment-program", "").toString(); + settings.endGroup(); + + if (useProgram && (program != "")) { + return alignModelViaProgram(ref, other, program); + } else { + return alignModelViaTransform(ref, other); + } +} + +QString +Align::getAlignmentTransformName() +{ + QSettings settings; + settings.beginGroup("Alignment"); + TransformId id = + settings.value("transform-id", + "vamp:match-vamp-plugin:match:path").toString(); + settings.endGroup(); + return id; +} + +bool +Align::canAlign() +{ + TransformId id = getAlignmentTransformName(); + TransformFactory *factory = TransformFactory::getInstance(); + return factory->haveTransform(id); +} + +bool +Align::alignModelViaTransform(Model *ref, Model *other) +{ + RangeSummarisableTimeValueModel *reference = qobject_cast + <RangeSummarisableTimeValueModel *>(ref); + + RangeSummarisableTimeValueModel *rm = qobject_cast + <RangeSummarisableTimeValueModel *>(other); + + if (!reference || !rm) return false; // but this should have been tested already + + // This involves creating three new models: + + // 1. an AggregateWaveModel to provide the mixdowns of the main + // model and the new model in its two channels, as input to the + // MATCH plugin + + // 2. a SparseTimeValueModel, which is the model automatically + // created by FeatureExtractionPluginTransformer when running the + // MATCH plugin (thus containing the alignment path) + + // 3. an AlignmentModel, which stores the path model and carries + // out alignment lookups on it. + + // The first two of these are provided as arguments to the + // constructor for the third, which takes responsibility for + // deleting them. The AlignmentModel, meanwhile, is passed to the + // new model we are aligning, which also takes responsibility for + // it. We should not have to delete any of these new models here. + + AggregateWaveModel::ChannelSpecList components; + + components.push_back(AggregateWaveModel::ModelChannelSpec + (reference, -1)); + + components.push_back(AggregateWaveModel::ModelChannelSpec + (rm, -1)); + + Model *aggregateModel = new AggregateWaveModel(components); + ModelTransformer::Input aggregate(aggregateModel); + + TransformId id = getAlignmentTransformName(); + + TransformFactory *tf = TransformFactory::getInstance(); + + Transform transform = tf->getDefaultTransformFor + (id, aggregateModel->getSampleRate()); + + transform.setStepSize(transform.getBlockSize()/2); + transform.setParameter("serialise", 1); + transform.setParameter("smooth", 0); + + SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; + + ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); + + QString message; + Model *transformOutput = mtf->transform(transform, aggregate, message); + + if (!transformOutput) { + transform.setStepSize(0); + transformOutput = mtf->transform(transform, aggregate, message); + } + + SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *> + (transformOutput); + + if (!path) { + cerr << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl; + delete transformOutput; + delete aggregateModel; + m_error = message; + return false; + } + + path->setCompletion(0); + + AlignmentModel *alignmentModel = new AlignmentModel + (reference, other, aggregateModel, path); + + connect(alignmentModel, SIGNAL(completionChanged()), + this, SLOT(alignmentCompletionChanged())); + + rm->setAlignment(alignmentModel); + + return true; +} + +void +Align::alignmentCompletionChanged() +{ + AlignmentModel *am = qobject_cast<AlignmentModel *>(sender()); + if (!am) return; + if (am->isReady()) { + disconnect(am, SIGNAL(completionChanged()), + this, SLOT(alignmentCompletionChanged())); + emit alignmentComplete(am); + } +} + +bool +Align::alignModelViaProgram(Model *ref, Model *other, QString program) +{ + WaveFileModel *reference = qobject_cast<WaveFileModel *>(ref); + WaveFileModel *rm = qobject_cast<WaveFileModel *>(other); + + if (!reference || !rm) { + return false; // but this should have been tested already + } + + while (!reference->isReady(0) || !rm->isReady(0)) { + qApp->processEvents(); + } + + // Run an external program, passing to it paths to the main + // model's audio file and the new model's audio file. It returns + // the path in CSV form through stdout. + + ReadOnlyWaveFileModel *roref = qobject_cast<ReadOnlyWaveFileModel *>(reference); + ReadOnlyWaveFileModel *rorm = qobject_cast<ReadOnlyWaveFileModel *>(rm); + if (!roref || !rorm) { + cerr << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl; + return false; + } + + QString refPath = roref->getLocalFilename(); + QString otherPath = rorm->getLocalFilename(); + + if (refPath == "" || otherPath == "") { + m_error = "Failed to find local filepath for wave-file model"; + return false; + } + + m_error = ""; + + AlignmentModel *alignmentModel = new AlignmentModel(reference, other, 0, 0); + rm->setAlignment(alignmentModel); + + QProcess *process = new QProcess; + QStringList args; + args << refPath << otherPath; + + connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), + this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus))); + + m_processModels[process] = alignmentModel; + process->start(program, args); + + bool success = process->waitForStarted(); + + if (!success) { + cerr << "ERROR: Align::alignModelViaProgram: Program did not start" + << endl; + m_error = "Alignment program could not be started"; + m_processModels.erase(process); + rm->setAlignment(0); // deletes alignmentModel as well + delete process; + } + + return success; +} + +void +Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status) +{ + cerr << "Align::alignmentProgramFinished" << endl; + + QProcess *process = qobject_cast<QProcess *>(sender()); + + if (m_processModels.find(process) == m_processModels.end()) { + cerr << "ERROR: Align::alignmentProgramFinished: Process " << process + << " not found in process model map!" << endl; + return; + } + + AlignmentModel *alignmentModel = m_processModels[process]; + + if (exitCode == 0 && status == 0) { + + CSVFormat format; + format.setModelType(CSVFormat::TwoDimensionalModel); + format.setTimingType(CSVFormat::ExplicitTiming); + format.setTimeUnits(CSVFormat::TimeSeconds); + format.setColumnCount(2); + // The output format has time in the reference file first, and + // time in the "other" file in the second column. This is a + // more natural approach for a command-line alignment tool, + // but it's the opposite of what we expect for native + // alignment paths, which map from "other" file to + // reference. These column purpose settings reflect that. + format.setColumnPurpose(1, CSVFormat::ColumnStartTime); + format.setColumnPurpose(0, CSVFormat::ColumnValue); + format.setAllowQuoting(false); + format.setSeparator(','); + + CSVFileReader reader(process, format, alignmentModel->getSampleRate()); + if (!reader.isOK()) { + cerr << "ERROR: Align::alignmentProgramFinished: Failed to parse output" + << endl; + m_error = QString("Failed to parse output of program: %1") + .arg(reader.getError()); + goto done; + } + + Model *csvOutput = reader.load(); + + SparseTimeValueModel *path = qobject_cast<SparseTimeValueModel *>(csvOutput); + if (!path) { + cerr << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model" + << endl; + m_error = QString("Output of program did not produce sparse time-value model"); + goto done; + } + + if (path->getPoints().empty()) { + cerr << "ERROR: Align::alignmentProgramFinished: Output contained no mappings" + << endl; + m_error = QString("Output of alignment program contained no mappings"); + goto done; + } + + cerr << "Align::alignmentProgramFinished: Setting alignment path (" + << path->getPoints().size() << " point(s))" << endl; + + alignmentModel->setPathFrom(path); + + emit alignmentComplete(alignmentModel); + + } else { + cerr << "ERROR: Align::alignmentProgramFinished: Aligner program " + << "failed: exit code " << exitCode << ", status " << status + << endl; + m_error = "Aligner process returned non-zero exit status"; + } + +done: + m_processModels.erase(process); + delete process; +} +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/framework/Align.h Thu Apr 21 15:06:43 2016 +0100 @@ -0,0 +1,83 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam and QMUL. + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of the + License, or (at your option) any later version. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef ALIGN_H +#define ALIGN_H + +#include <QString> +#include <QObject> +#include <QProcess> +#include <set> + +class Model; +class AlignmentModel; + +class Align : public QObject +{ + Q_OBJECT + +public: + Align() : m_error("") { } + + /** + * Align the "other" model to the reference, attaching an + * AlignmentModel to it. Alignment is carried out by the method + * configured in the user preferences (either a plugin transform + * or an external process) and is done asynchronously. + * + * A single Align object may carry out many simultanous alignment + * calls -- you do not need to create a new Align object each + * time, nor to wait for an alignment to be complete before + * starting a new one. + * + * The Align object must survive after this call, for at least as + * long as the alignment takes. The usual expectation is that the + * Align object will simply share the process or document + * lifespan. + */ + bool alignModel(Model *reference, Model *other); // via user preference + + bool alignModelViaTransform(Model *reference, Model *other); + bool alignModelViaProgram(Model *reference, Model *other, QString program); + + /** + * Return true if the alignment facility is available (relevant + * plugin installed, etc). + */ + static bool canAlign(); + + QString getError() const { return m_error; } + +signals: + /** + * Emitted when an alignment is successfully completed. The + * reference and other models can be queried from the alignment + * model. + */ + void alignmentComplete(AlignmentModel *alignment); + +private slots: + void alignmentCompletionChanged(); + void alignmentProgramFinished(int, QProcess::ExitStatus); + +private: + static QString getAlignmentTransformName(); + + QString m_error; + std::map<QProcess *, AlignmentModel *> m_processModels; +}; + +#endif +
--- a/framework/Document.cpp Fri Mar 18 14:25:05 2016 +0000 +++ b/framework/Document.cpp Thu Apr 21 15:06:43 2016 +0100 @@ -15,6 +15,8 @@ #include "Document.h" +#include "Align.h" + #include "data/model/WaveFileModel.h" #include "data/model/WritableWaveFileModel.h" #include "data/model/DenseThreeDimensionalModel.h" @@ -36,10 +38,8 @@ #include <iostream> #include <typeinfo> -// For alignment: -#include "data/model/AggregateWaveModel.h" -#include "data/model/SparseTimeValueModel.h" #include "data/model/AlignmentModel.h" +#include "Align.h" using std::vector; @@ -49,7 +49,8 @@ Document::Document() : m_mainModel(0), - m_autoAlignment(false) + m_autoAlignment(false), + m_align(new Align()) { connect(this, SIGNAL(modelAboutToBeDeleted(Model *)), @@ -60,6 +61,9 @@ SIGNAL(transformFailed(QString, QString)), this, SIGNAL(modelGenerationFailed(QString, QString))); + + connect(m_align, SIGNAL(alignmentComplete(AlignmentModel *)), + this, SIGNAL(alignmentComplete(AlignmentModel *))); } Document::~Document() @@ -1038,24 +1042,10 @@ return (m_models.find(const_cast<Model *>(model)) != m_models.end()); } -TransformId -Document::getAlignmentTransformName() +bool +Document::canAlign() { - QSettings settings; - settings.beginGroup("Alignment"); - TransformId id = - settings.value("transform-id", - "vamp:match-vamp-plugin:match:path").toString(); - settings.endGroup(); - return id; -} - -bool -Document::canAlign() -{ - TransformId id = getAlignmentTransformName(); - TransformFactory *factory = TransformFactory::getInstance(); - return factory->haveTransform(id); + return Align::canAlign(); } void @@ -1090,75 +1080,10 @@ return; } - // This involves creating three new models: - - // 1. an AggregateWaveModel to provide the mixdowns of the main - // model and the new model in its two channels, as input to the - // MATCH plugin - - // 2. a SparseTimeValueModel, which is the model automatically - // created by FeatureExtractionPluginTransformer when running the - // MATCH plugin (thus containing the alignment path) - - // 3. an AlignmentModel, which stores the path model and carries - // out alignment lookups on it. - - // The first two of these are provided as arguments to the - // constructor for the third, which takes responsibility for - // deleting them. The AlignmentModel, meanwhile, is passed to the - // new model we are aligning, which also takes responsibility for - // it. We should not have to delete any of these new models here. - - AggregateWaveModel::ChannelSpecList components; - - components.push_back(AggregateWaveModel::ModelChannelSpec - (m_mainModel, -1)); - - components.push_back(AggregateWaveModel::ModelChannelSpec - (rm, -1)); - - Model *aggregateModel = new AggregateWaveModel(components); - ModelTransformer::Input aggregate(aggregateModel); - - TransformId id = "vamp:match-vamp-plugin:match:path"; //!!! configure - - TransformFactory *tf = TransformFactory::getInstance(); - - Transform transform = tf->getDefaultTransformFor - (id, aggregateModel->getSampleRate()); - - transform.setStepSize(transform.getBlockSize()/2); - transform.setParameter("serialise", 1); - - SVDEBUG << "Document::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; - - ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); - - QString message; - Model *transformOutput = mtf->transform(transform, aggregate, message); - - if (!transformOutput) { - transform.setStepSize(0); - transformOutput = mtf->transform(transform, aggregate, message); + if (!m_align->alignModel(m_mainModel, rm)) { + cerr << "Alignment failed: " << m_align->getError() << endl; + emit alignmentFailed(m_align->getError()); } - - SparseTimeValueModel *path = dynamic_cast<SparseTimeValueModel *> - (transformOutput); - - if (!path) { - cerr << "Document::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl; - emit alignmentFailed(id, message); - delete transformOutput; - delete aggregateModel; - return; - } - - path->setCompletion(0); - - AlignmentModel *alignmentModel = new AlignmentModel - (m_mainModel, model, aggregateModel, path); - - rm->setAlignment(alignmentModel); } void
--- a/framework/Document.h Fri Mar 18 14:25:05 2016 +0000 +++ b/framework/Document.h Thu Apr 21 15:06:43 2016 +0100 @@ -32,6 +32,8 @@ class AdditionalModelConverter; +class Align; + /** * A Sonic Visualiser document consists of a set of data models, and * also the visualisation layers used to display them. Changes to the @@ -301,7 +303,9 @@ QString message); void modelRegenerationWarning(QString layerName, QString transformName, QString message); - void alignmentFailed(QString transformName, QString message); + + void alignmentComplete(AlignmentModel *); + void alignmentFailed(QString message); void activity(QString); @@ -407,8 +411,6 @@ void writeBackwardCompatibleDerivation(QTextStream &, QString, Model *, const ModelRecord &) const; - static TransformId getAlignmentTransformName(); - void toXml(QTextStream &, QString, QString, bool asTemplate) const; void writePlaceholderMainModel(QTextStream &, QString) const; @@ -423,6 +425,7 @@ LayerSet m_layers; bool m_autoAlignment; + Align *m_align; }; #endif
--- a/framework/MainWindowBase.cpp Fri Mar 18 14:25:05 2016 +0000 +++ b/framework/MainWindowBase.cpp Thu Apr 21 15:06:43 2016 +0100 @@ -157,6 +157,7 @@ m_defaultFfwdRwdStep(2, 0), m_audioRecordMode(RecordCreateAdditionalModel), m_statusLabel(0), + m_iconsVisibleInMenus(true), m_menuShortcutMapper(0) { Profiler profiler("MainWindowBase::MainWindowBase"); @@ -334,12 +335,12 @@ } void -MainWindowBase::finaliseMenu(QMenu * -#ifdef Q_OS_MAC - menu -#endif - ) +MainWindowBase::finaliseMenu(QMenu *menu) { + foreach (QAction *a, menu->actions()) { + a->setIconVisibleInMenu(m_iconsVisibleInMenus); + } + #ifdef Q_OS_MAC // See https://bugreports.qt-project.org/browse/QTBUG-38256 and // our issue #890 http://code.soundsoftware.ac.uk/issues/890 -- @@ -1787,6 +1788,51 @@ } MainWindowBase::FileOpenStatus +MainWindowBase::openDirOfAudio(QString dirPath) +{ + QDir dir(dirPath); + QStringList files = dir.entryList(QDir::Files | QDir::Readable); + files.sort(); + + FileOpenStatus status = FileOpenFailed; + bool first = true; + bool cancelled = false; + + foreach (QString file, files) { + + FileSource source(dir.filePath(file)); + if (!source.isAvailable()) { + continue; + } + + if (AudioFileReaderFactory::getKnownExtensions().contains + (source.getExtension().toLower())) { + + AudioFileOpenMode mode = CreateAdditionalModel; + if (first) mode = ReplaceSession; + + switch (openAudio(source, mode)) { + case FileOpenSucceeded: + status = FileOpenSucceeded; + first = false; + break; + case FileOpenFailed: + break; + case FileOpenCancelled: + cancelled = true; + break; + case FileOpenWrongMode: + break; + } + } + + if (cancelled) break; + } + + return status; +} + +MainWindowBase::FileOpenStatus MainWindowBase::openSessionPath(QString fileOrUrl) { ProgressDialog dialog(tr("Opening session..."), true, 2000, this); @@ -2273,8 +2319,10 @@ this, SLOT(modelGenerationFailed(QString, QString))); connect(m_document, SIGNAL(modelRegenerationWarning(QString, QString, QString)), this, SLOT(modelRegenerationWarning(QString, QString, QString))); - connect(m_document, SIGNAL(alignmentFailed(QString, QString)), - this, SLOT(alignmentFailed(QString, QString))); + connect(m_document, SIGNAL(alignmentComplete(AlignmentModel *)), + this, SLOT(alignmentComplete(AlignmentModel *))); + connect(m_document, SIGNAL(alignmentFailed(QString)), + this, SLOT(alignmentFailed(QString))); emit replacedDocument(); } @@ -2658,7 +2706,8 @@ void MainWindowBase::play() { - if (m_recordTarget->isRecording() || m_playSource->isPlaying()) { + if ((m_recordTarget && m_recordTarget->isRecording()) || + (m_playSource && m_playSource->isPlaying())) { stop(); QAction *action = qobject_cast<QAction *>(sender()); if (action) action->setChecked(false); @@ -3037,10 +3086,13 @@ void MainWindowBase::stop() { - if (m_recordTarget->isRecording()) { + if (m_recordTarget && + m_recordTarget->isRecording()) { m_recordTarget->stopRecording(); } - + + if (!m_playSource) return; + m_playSource->stop(); if (m_audioIO) m_audioIO->suspend(); @@ -3578,6 +3630,12 @@ } void +MainWindowBase::alignmentComplete(AlignmentModel *model) +{ + cerr << "MainWindowBase::alignmentComplete(" << model << ")" << endl; +} + +void MainWindowBase::pollOSC() { if (!m_oscQueue || m_oscQueue->isEmpty()) return;
--- a/framework/MainWindowBase.h Fri Mar 18 14:25:05 2016 +0000 +++ b/framework/MainWindowBase.h Thu Apr 21 15:06:43 2016 +0100 @@ -62,6 +62,7 @@ class ModelDataTableDialog; class QSignalMapper; class QShortcut; +class AlignmentModel; namespace breakfastquay { class SystemPlaybackTarget; @@ -86,7 +87,8 @@ WithAudioOutput = 0x01, WithAudioInput = 0x02, WithMIDIInput = 0x04, - WithEverything = 0xff + WithEverything = 0xff, + WithNothing = 0x00 }; typedef int SoundOptions; @@ -120,6 +122,8 @@ virtual FileOpenStatus openLayer(FileSource source); virtual FileOpenStatus openImage(FileSource source); + virtual FileOpenStatus openDirOfAudio(QString dirPath); + virtual FileOpenStatus openSession(FileSource source); virtual FileOpenStatus openSessionPath(QString fileOrUrl); virtual FileOpenStatus openSessionTemplate(QString templateName); @@ -291,7 +295,9 @@ virtual void modelGenerationWarning(QString, QString) = 0; virtual void modelRegenerationFailed(QString, QString, QString) = 0; virtual void modelRegenerationWarning(QString, QString, QString) = 0; - virtual void alignmentFailed(QString, QString) = 0; + + virtual void alignmentComplete(AlignmentModel *); + virtual void alignmentFailed(QString) = 0; virtual void rightButtonMenuRequested(Pane *, QPoint point) = 0; @@ -460,10 +466,14 @@ virtual void updatePositionStatusDisplays() const = 0; // Call this after setting up the menu bar, to fix up single-key - // shortcuts on OS/X + // shortcuts on OS/X and do any other platform-specific tidying virtual void finaliseMenus(); virtual void finaliseMenu(QMenu *); + // Call before finaliseMenus if you wish to have a say in this question + void setIconsVisibleInMenus(bool visible) { m_iconsVisibleInMenus = visible; } + bool m_iconsVisibleInMenus; + // Only used on OS/X to work around a Qt/Cocoa bug, see finaliseMenus QSignalMapper *m_menuShortcutMapper; QList<QShortcut *> m_appShortcuts;
--- a/svapp.pro Fri Mar 18 14:25:05 2016 +0000 +++ b/svapp.pro Thu Apr 21 15:06:43 2016 +0100 @@ -57,13 +57,15 @@ audio/ContinuousSynth.cpp \ audio/PlaySpeedRangeMapper.cpp -HEADERS += framework/Document.h \ +HEADERS += framework/Align.h \ + framework/Document.h \ framework/MainWindowBase.h \ framework/SVFileReader.h \ framework/TransformUserConfigurator.h \ framework/VersionTester.h -SOURCES += framework/Document.cpp \ +SOURCES += framework/Align.cpp \ + framework/Document.cpp \ framework/MainWindowBase.cpp \ framework/SVFileReader.cpp \ framework/TransformUserConfigurator.cpp \