diff align/ExternalProgramAligner.cpp @ 752:32654e402f8b pitch-align

Pull out ExternalProgramAligner and TransformAligner from Align - currently duplicating the code, the pulled-out classes are not yet in use
author Chris Cannam
date Thu, 23 Apr 2020 17:11:26 +0100
parents
children 6429a164b7e1
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/align/ExternalProgramAligner.cpp	Thu Apr 23 17:11:26 2020 +0100
@@ -0,0 +1,224 @@
+/* -*- 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 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 "ExternalProgramAligner.h"
+
+#include <QFileInfo>
+#include <QApplication>
+
+#include "data/model/ReadOnlyWaveFileModel.h"
+#include "data/model/SparseTimeValueModel.h"
+#include "data/model/AlignmentModel.h"
+
+#include "data/fileio/CSVFileReader.h"
+
+#include "framework/Document.h"
+
+ExternalProgramAligner::ExternalProgramAligner(Document *doc,
+                                               ModelId reference,
+                                               ModelId toAlign,
+                                               QString program) :
+    m_document(doc),
+    m_reference(reference),
+    m_toAlign(toAlign),
+    m_program(program),
+    m_process(nullptr)
+{
+}
+
+ExternalProgramAligner::~ExternalProgramAligner()
+{
+    delete m_process;
+}
+
+bool
+ExternalProgramAligner::isAvailable(QString program)
+{
+    QFileInfo file(program);
+    return file.exists() && file.isExecutable();
+}
+
+bool
+ExternalProgramAligner::begin(QString &error)
+{
+    // 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.
+
+    auto reference = ModelById::getAs<ReadOnlyWaveFileModel>(m_reference);
+    auto other = ModelById::getAs<ReadOnlyWaveFileModel>(m_toAlign);
+    if (!reference || !other) {
+        SVCERR << "ERROR: ExternalProgramAligner: Can't align non-read-only models via program (no local filename available)" << endl;
+        return false;
+    }
+
+    while (!reference->isReady(nullptr) || !other->isReady(nullptr)) {
+        qApp->processEvents();
+    }
+    
+    QString refPath = reference->getLocalFilename();
+    if (refPath == "") {
+        refPath = FileSource(reference->getLocation()).getLocalFilename();
+    }
+    
+    QString otherPath = other->getLocalFilename();
+    if (otherPath == "") {
+        otherPath = FileSource(other->getLocation()).getLocalFilename();
+    }
+
+    if (refPath == "" || otherPath == "") {
+        error = "Failed to find local filepath for wave-file model";
+        return false;
+    }
+
+    auto alignmentModel =
+        std::make_shared<AlignmentModel>(m_reference, m_toAlign, ModelId());
+
+    m_alignmentModel = ModelById::add(alignmentModel);
+    other->setAlignment(m_alignmentModel);
+
+    m_process = new QProcess;
+    m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel);
+
+    connect(m_process,
+            SIGNAL(finished(int, QProcess::ExitStatus)),
+            this,
+            SLOT(programFinished(int, QProcess::ExitStatus)));
+
+    QStringList args;
+    args << refPath << otherPath;
+
+    SVCERR << "ExternalProgramAligner: Starting program \""
+           << m_program << "\" with args: ";
+    for (auto a: args) {
+        SVCERR << "\"" << a << "\" ";
+    }
+    SVCERR << endl;
+
+    m_process->start(m_program, args);
+
+    bool success = m_process->waitForStarted();
+
+    if (!success) {
+        
+        SVCERR << "ERROR: ExternalProgramAligner: Program did not start" << endl;
+        error = "Alignment program \"" + m_program + "\" did not start";
+        
+        other->setAlignment({});
+        ModelById::release(m_alignmentModel);
+        delete m_process;
+        m_process = nullptr;
+        
+    } else {
+        m_document->addNonDerivedModel(m_alignmentModel);
+    }
+
+    return success;
+}
+
+void
+ExternalProgramAligner::programFinished(int  exitCode, QProcess::ExitStatus status)
+{
+    SVCERR << "ExternalProgramAligner::programFinished" << endl;
+    
+    QProcess *process = qobject_cast<QProcess *>(sender());
+
+    if (process != m_process) {
+        SVCERR << "ERROR: ExternalProgramAligner: Emitting process " << process
+               << " is not my process!" << endl;
+        return;
+    }
+
+    auto alignmentModel = ModelById::getAs<AlignmentModel>(m_alignmentModel);
+    if (!alignmentModel) {
+        SVCERR << "ExternalProgramAligner: AlignmentModel no longer exists"
+               << endl;
+        return;
+    }
+    
+    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()) {
+            SVCERR << "ERROR: ExternalProgramAligner: Failed to parse output"
+                   << endl;
+            alignmentModel->setError
+                (QString("Failed to parse output of program: %1")
+                 .arg(reader.getError()));
+            goto done;
+        }
+
+        //!!! to use ById?
+        
+        Model *csvOutput = reader.load();
+
+        SparseTimeValueModel *path =
+            qobject_cast<SparseTimeValueModel *>(csvOutput);
+        if (!path) {
+            SVCERR << "ERROR: ExternalProgramAligner: Output did not convert to sparse time-value model"
+                   << endl;
+            alignmentModel->setError
+                ("Output of program did not produce sparse time-value model");
+            delete csvOutput;
+            goto done;
+        }
+                       
+        if (path->isEmpty()) {
+            SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings"
+                   << endl;
+            alignmentModel->setError
+                ("Output of alignment program contained no mappings");
+            delete path;
+            goto done;
+        }
+
+        SVCERR << "ExternalProgramAligner: Setting alignment path ("
+             << path->getEventCount() << " point(s))" << endl;
+
+        auto pathId =
+            ModelById::add(std::shared_ptr<SparseTimeValueModel>(path));
+        alignmentModel->setPathFrom(pathId);
+
+        emit complete(m_alignmentModel);
+
+        ModelById::release(pathId);
+        
+    } else {
+        SVCERR << "ERROR: ExternalProgramAligner: Aligner program "
+               << "failed: exit code " << exitCode << ", status " << status
+               << endl;
+        alignmentModel->setError
+            ("Aligner process returned non-zero exit status");
+    }
+
+done:
+    delete m_process;
+    m_process = nullptr;
+}