changeset 515:51befd6165a3 alignment-simple

Merge in from SV 3.0-integration branches
author Chris Cannam
date Wed, 02 Mar 2016 17:25:27 +0000
parents 74d575708e06 (diff) 68ab0fe3bce4 (current diff)
children b926f08909b8
files audioio/AudioCallbackPlaySource.cpp audioio/AudioCallbackPlaySource.h audioio/AudioCallbackPlayTarget.cpp audioio/AudioCallbackPlayTarget.h audioio/AudioGenerator.cpp audioio/AudioGenerator.h audioio/AudioJACKTarget.cpp audioio/AudioJACKTarget.h audioio/AudioPortAudioTarget.cpp audioio/AudioPortAudioTarget.h audioio/AudioPulseAudioTarget.cpp audioio/AudioPulseAudioTarget.h audioio/AudioTargetFactory.cpp audioio/AudioTargetFactory.h audioio/ClipMixer.cpp audioio/ClipMixer.h audioio/ContinuousSynth.cpp audioio/ContinuousSynth.h audioio/PlaySpeedRangeMapper.cpp audioio/PlaySpeedRangeMapper.h framework/Align.cpp framework/MainWindowBase.cpp framework/MainWindowBase.h svapp.pro
diffstat 7 files changed, 491 insertions(+), 104 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/framework/Align.cpp	Wed Mar 02 17:25:27 2016 +0000
@@ -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	Wed Mar 02 17:25:27 2016 +0000
@@ -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	Thu Feb 04 11:19:01 2016 +0000
+++ b/framework/Document.cpp	Wed Mar 02 17:25:27 2016 +0000
@@ -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	Thu Feb 04 11:19:01 2016 +0000
+++ b/framework/Document.h	Wed Mar 02 17:25:27 2016 +0000
@@ -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	Thu Feb 04 11:19:01 2016 +0000
+++ b/framework/MainWindowBase.cpp	Wed Mar 02 17:25:27 2016 +0000
@@ -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();
 }
@@ -3578,6 +3626,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	Thu Feb 04 11:19:01 2016 +0000
+++ b/framework/MainWindowBase.h	Wed Mar 02 17:25:27 2016 +0000
@@ -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	Thu Feb 04 11:19:01 2016 +0000
+++ b/svapp.pro	Wed Mar 02 17:25:27 2016 +0000
@@ -54,13 +54,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 \