Chris@752: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@752: Chris@752: /* Chris@752: Sonic Visualiser Chris@752: An audio file viewer and annotation editor. Chris@752: Centre for Digital Music, Queen Mary, University of London. Chris@752: Chris@752: This program is free software; you can redistribute it and/or Chris@752: modify it under the terms of the GNU General Public License as Chris@752: published by the Free Software Foundation; either version 2 of the Chris@752: License, or (at your option) any later version. See the file Chris@752: COPYING included with this distribution for more information. Chris@752: */ Chris@752: Chris@752: #include "ExternalProgramAligner.h" Chris@752: Chris@752: #include Chris@752: #include Chris@752: Chris@752: #include "data/model/ReadOnlyWaveFileModel.h" Chris@752: #include "data/model/SparseTimeValueModel.h" Chris@752: #include "data/model/AlignmentModel.h" Chris@752: Chris@752: #include "data/fileio/CSVFileReader.h" Chris@752: Chris@752: #include "framework/Document.h" Chris@752: Chris@752: ExternalProgramAligner::ExternalProgramAligner(Document *doc, Chris@752: ModelId reference, Chris@752: ModelId toAlign, Chris@752: QString program) : Chris@752: m_document(doc), Chris@752: m_reference(reference), Chris@752: m_toAlign(toAlign), Chris@752: m_program(program), Chris@752: m_process(nullptr) Chris@752: { Chris@752: } Chris@752: Chris@752: ExternalProgramAligner::~ExternalProgramAligner() Chris@752: { Chris@752: delete m_process; Chris@752: } Chris@752: Chris@752: bool Chris@752: ExternalProgramAligner::isAvailable(QString program) Chris@752: { Chris@752: QFileInfo file(program); Chris@752: return file.exists() && file.isExecutable(); Chris@752: } Chris@752: Chris@761: void Chris@761: ExternalProgramAligner::begin() Chris@752: { Chris@752: // Run an external program, passing to it paths to the main Chris@752: // model's audio file and the new model's audio file. It returns Chris@752: // the path in CSV form through stdout. Chris@752: Chris@752: auto reference = ModelById::getAs(m_reference); Chris@752: auto other = ModelById::getAs(m_toAlign); Chris@752: if (!reference || !other) { Chris@752: SVCERR << "ERROR: ExternalProgramAligner: Can't align non-read-only models via program (no local filename available)" << endl; Chris@761: return; Chris@752: } Chris@752: Chris@752: while (!reference->isReady(nullptr) || !other->isReady(nullptr)) { Chris@752: qApp->processEvents(); Chris@752: } Chris@752: Chris@752: QString refPath = reference->getLocalFilename(); Chris@752: if (refPath == "") { Chris@752: refPath = FileSource(reference->getLocation()).getLocalFilename(); Chris@752: } Chris@752: Chris@752: QString otherPath = other->getLocalFilename(); Chris@752: if (otherPath == "") { Chris@752: otherPath = FileSource(other->getLocation()).getLocalFilename(); Chris@752: } Chris@752: Chris@752: if (refPath == "" || otherPath == "") { Chris@761: emit failed(m_toAlign, Chris@761: tr("Failed to find local filepath for wave-file model")); Chris@761: return; Chris@752: } Chris@752: Chris@752: auto alignmentModel = Chris@752: std::make_shared(m_reference, m_toAlign, ModelId()); Chris@752: Chris@752: m_alignmentModel = ModelById::add(alignmentModel); Chris@752: other->setAlignment(m_alignmentModel); Chris@752: Chris@752: m_process = new QProcess; Chris@752: m_process->setProcessChannelMode(QProcess::ForwardedErrorChannel); Chris@752: Chris@752: connect(m_process, Chris@752: SIGNAL(finished(int, QProcess::ExitStatus)), Chris@752: this, Chris@752: SLOT(programFinished(int, QProcess::ExitStatus))); Chris@752: Chris@752: QStringList args; Chris@752: args << refPath << otherPath; Chris@752: Chris@752: SVCERR << "ExternalProgramAligner: Starting program \"" Chris@752: << m_program << "\" with args: "; Chris@752: for (auto a: args) { Chris@752: SVCERR << "\"" << a << "\" "; Chris@752: } Chris@752: SVCERR << endl; Chris@752: Chris@752: m_process->start(m_program, args); Chris@752: Chris@752: bool success = m_process->waitForStarted(); Chris@752: Chris@752: if (!success) { Chris@752: Chris@752: SVCERR << "ERROR: ExternalProgramAligner: Program did not start" << endl; Chris@761: emit failed(m_toAlign, Chris@761: tr("Alignment program \"%1\" did not start") Chris@761: .arg(m_program)); Chris@752: Chris@752: other->setAlignment({}); Chris@752: ModelById::release(m_alignmentModel); Chris@752: delete m_process; Chris@752: m_process = nullptr; Chris@752: Chris@752: } else { Chris@752: m_document->addNonDerivedModel(m_alignmentModel); Chris@752: } Chris@752: } Chris@752: Chris@752: void Chris@752: ExternalProgramAligner::programFinished(int exitCode, QProcess::ExitStatus status) Chris@752: { Chris@752: SVCERR << "ExternalProgramAligner::programFinished" << endl; Chris@752: Chris@752: QProcess *process = qobject_cast(sender()); Chris@752: Chris@752: if (process != m_process) { Chris@752: SVCERR << "ERROR: ExternalProgramAligner: Emitting process " << process Chris@752: << " is not my process!" << endl; Chris@752: return; Chris@752: } Chris@752: Chris@752: auto alignmentModel = ModelById::getAs(m_alignmentModel); Chris@752: if (!alignmentModel) { Chris@752: SVCERR << "ExternalProgramAligner: AlignmentModel no longer exists" Chris@752: << endl; Chris@752: return; Chris@752: } Chris@752: Chris@752: if (exitCode == 0 && status == 0) { Chris@752: Chris@752: CSVFormat format; Chris@752: format.setModelType(CSVFormat::TwoDimensionalModel); Chris@752: format.setTimingType(CSVFormat::ExplicitTiming); Chris@752: format.setTimeUnits(CSVFormat::TimeSeconds); Chris@752: format.setColumnCount(2); Chris@752: // The output format has time in the reference file first, and Chris@752: // time in the "other" file in the second column. This is a Chris@752: // more natural approach for a command-line alignment tool, Chris@752: // but it's the opposite of what we expect for native Chris@752: // alignment paths, which map from "other" file to Chris@752: // reference. These column purpose settings reflect that. Chris@752: format.setColumnPurpose(1, CSVFormat::ColumnStartTime); Chris@752: format.setColumnPurpose(0, CSVFormat::ColumnValue); Chris@752: format.setAllowQuoting(false); Chris@752: format.setSeparator(','); Chris@752: Chris@752: CSVFileReader reader(process, format, alignmentModel->getSampleRate()); Chris@752: if (!reader.isOK()) { Chris@752: SVCERR << "ERROR: ExternalProgramAligner: Failed to parse output" Chris@752: << endl; Chris@761: QString error = tr("Failed to parse output of program: %1") Chris@761: .arg(reader.getError()); Chris@761: alignmentModel->setError(error); Chris@761: emit failed(m_toAlign, error); Chris@752: goto done; Chris@752: } Chris@752: Chris@752: //!!! to use ById? Chris@752: Chris@752: Model *csvOutput = reader.load(); Chris@752: Chris@752: SparseTimeValueModel *path = Chris@752: qobject_cast(csvOutput); Chris@752: if (!path) { Chris@752: SVCERR << "ERROR: ExternalProgramAligner: Output did not convert to sparse time-value model" Chris@752: << endl; Chris@761: QString error = Chris@761: tr("Output of alignment program was not in the proper format"); Chris@761: alignmentModel->setError(error); Chris@752: delete csvOutput; Chris@761: emit failed(m_toAlign, error); Chris@752: goto done; Chris@752: } Chris@752: Chris@752: if (path->isEmpty()) { Chris@752: SVCERR << "ERROR: ExternalProgramAligner: Output contained no mappings" Chris@752: << endl; Chris@761: QString error = Chris@761: tr("Output of alignment program contained no mappings"); Chris@761: alignmentModel->setError(error); Chris@752: delete path; Chris@761: emit failed(m_toAlign, error); Chris@752: goto done; Chris@752: } Chris@752: Chris@752: SVCERR << "ExternalProgramAligner: Setting alignment path (" Chris@752: << path->getEventCount() << " point(s))" << endl; Chris@752: Chris@752: auto pathId = Chris@752: ModelById::add(std::shared_ptr(path)); Chris@752: alignmentModel->setPathFrom(pathId); Chris@752: Chris@752: emit complete(m_alignmentModel); Chris@752: Chris@752: ModelById::release(pathId); Chris@752: Chris@752: } else { Chris@752: SVCERR << "ERROR: ExternalProgramAligner: Aligner program " Chris@752: << "failed: exit code " << exitCode << ", status " << status Chris@752: << endl; Chris@761: QString error = tr("Aligner process returned non-zero exit status"); Chris@761: alignmentModel->setError(error); Chris@761: emit failed(m_toAlign, error); Chris@752: } Chris@752: Chris@752: done: Chris@752: delete m_process; Chris@752: m_process = nullptr; Chris@752: }