view align/ExternalProgramAligner.cpp @ 769:a316cb6fed81 pitch-align

Fixes to notification and completion in aligners
author Chris Cannam
date Thu, 28 May 2020 17:04:36 +0100
parents 6429a164b7e1
children 699b5b130ea2
line wrap: on
line source
/* -*- 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();
}

void
ExternalProgramAligner::begin()
{
    // 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;
    }

    if (m_program == "") {
        emit failed(m_toAlign, tr("No external program specified"));
        return;
    }

    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 == "") {
        emit failed(m_toAlign,
                    tr("Failed to find local filepath for wave-file model"));
        return;
    }

    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;

        other->setAlignment({});
        ModelById::release(m_alignmentModel);
        delete m_process;
        m_process = nullptr;

        emit failed(m_toAlign,
                    tr("Alignment program \"%1\" did not start")
                    .arg(m_program));
        
    } else {
        alignmentModel->setCompletion(10);
        m_document->addNonDerivedModel(m_alignmentModel);
    }
}

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;
    }

    QString errorText;
    
    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;
            errorText = tr("Failed to parse output of program: %1")
                .arg(reader.getError());
            alignmentModel->setError(errorText);
            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;
            errorText =
                tr("Output of alignment program was not in the proper format");
            alignmentModel->setError(errorText);
            delete csvOutput;
            goto done;
        }
                       
        if (path->isEmpty()) {
            SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings"
                   << endl;
            errorText = 
                tr("Output of alignment program contained no mappings");
            alignmentModel->setError(errorText);
            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);
        alignmentModel->setCompletion(100);

        ModelById::release(pathId);
        
    } else {
        SVCERR << "ERROR: ExternalProgramAligner: Aligner program "
               << "failed: exit code " << exitCode << ", status " << status
               << endl;
        errorText = tr("Aligner process returned non-zero exit status");
        alignmentModel->setError(errorText);
    }

done:
    delete m_process;
    m_process = nullptr;

    // "This should be emitted as the last thing the aligner does, as
    // the recipient may delete the aligner during the call."
    if (errorText == "") {
        emit complete(m_alignmentModel);
    } else {
        emit failed(m_toAlign, errorText);
    }
}