Chris@420: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@420: Chris@420: /* Chris@420: Sonic Visualiser Chris@420: An audio file viewer and annotation editor. Chris@420: Centre for Digital Music, Queen Mary, University of London. Chris@420: Chris@420: This program is free software; you can redistribute it and/or Chris@420: modify it under the terms of the GNU General Public License as Chris@420: published by the Free Software Foundation; either version 2 of the Chris@420: License, or (at your option) any later version. See the file Chris@420: COPYING included with this distribution for more information. Chris@420: */ Chris@420: Chris@420: #include "Align.h" Chris@665: #include "Document.h" Chris@420: Chris@420: #include "data/model/WaveFileModel.h" Chris@515: #include "data/model/ReadOnlyWaveFileModel.h" Chris@420: #include "data/model/AggregateWaveModel.h" Chris@420: #include "data/model/RangeSummarisableTimeValueModel.h" Chris@420: #include "data/model/SparseTimeValueModel.h" Chris@420: #include "data/model/AlignmentModel.h" Chris@420: Chris@420: #include "data/fileio/CSVFileReader.h" Chris@420: Chris@420: #include "transform/TransformFactory.h" Chris@420: #include "transform/ModelTransformerFactory.h" Chris@420: #include "transform/FeatureExtractionModelTransformer.h" Chris@420: Chris@420: #include Chris@422: #include Chris@430: #include Chris@422: Chris@422: bool Chris@683: Align::alignModel(Document *doc, ModelId ref, ModelId other, QString &error) Chris@422: { Chris@422: QSettings settings; Chris@422: settings.beginGroup("Preferences"); Chris@422: bool useProgram = settings.value("use-external-alignment", false).toBool(); Chris@422: QString program = settings.value("external-alignment-program", "").toString(); Chris@422: settings.endGroup(); Chris@422: Chris@422: if (useProgram && (program != "")) { Chris@670: return alignModelViaProgram(doc, ref, other, program, error); Chris@422: } else { Chris@670: return alignModelViaTransform(doc, ref, other, error); Chris@422: } Chris@422: } Chris@420: Chris@428: QString Chris@428: Align::getAlignmentTransformName() Chris@428: { Chris@428: QSettings settings; Chris@428: settings.beginGroup("Alignment"); Chris@428: TransformId id = Chris@428: settings.value("transform-id", Chris@428: "vamp:match-vamp-plugin:match:path").toString(); Chris@428: settings.endGroup(); Chris@428: return id; Chris@428: } Chris@428: Chris@670: QString Chris@670: Align::getTuningDifferenceTransformName() Chris@670: { Chris@670: QSettings settings; Chris@670: settings.beginGroup("Alignment"); Chris@670: bool performPitchCompensation = Chris@670: settings.value("align-pitch-aware", false).toBool(); Chris@670: QString id = ""; Chris@671: if (performPitchCompensation) { Chris@670: id = settings.value Chris@670: ("tuning-difference-transform-id", Chris@670: "vamp:tuning-difference:tuning-difference:tuningfreq") Chris@670: .toString(); Chris@671: } Chris@670: settings.endGroup(); Chris@670: return id; Chris@670: } Chris@670: Chris@428: bool Chris@428: Align::canAlign() Chris@428: { Chris@670: TransformFactory *factory = TransformFactory::getInstance(); Chris@428: TransformId id = getAlignmentTransformName(); Chris@670: TransformId tdId = getTuningDifferenceTransformName(); Chris@670: return factory->haveTransform(id) && Chris@670: (tdId == "" || factory->haveTransform(tdId)); Chris@428: } Chris@428: Chris@420: bool Chris@687: Align::alignModelViaTransform(Document *doc, Chris@687: ModelId referenceId, Chris@687: ModelId otherId, Chris@670: QString &error) Chris@420: { Chris@670: QMutexLocker locker (&m_mutex); Chris@420: Chris@687: auto reference = Chris@687: ModelById::getAs(referenceId); Chris@687: auto other = Chris@687: ModelById::getAs(otherId); Chris@687: Chris@687: if (!reference || !other) return false; Chris@420: Chris@691: // This involves creating a number of new models: Chris@672: // Chris@420: // 1. an AggregateWaveModel to provide the mixdowns of the main Chris@420: // model and the new model in its two channels, as input to the Chris@691: // MATCH plugin. We just call this one aggregateModel Chris@672: // Chris@670: // 2a. a SparseTimeValueModel which will be automatically created Chris@670: // by FeatureExtractionModelTransformer when running the Chris@670: // TuningDifference plugin to receive the relative tuning of the Chris@670: // second model (if pitch-aware alignment is enabled in the Chris@691: // preferences). We call this tuningDiffOutputModel. Chris@672: // Chris@670: // 2b. a SparseTimeValueModel which will be automatically created Chris@670: // by FeatureExtractionPluginTransformer when running the MATCH Chris@691: // plugin to perform alignment (so containing the alignment path). Chris@691: // We call this one pathOutputModel. Chris@672: // Chris@691: // 2c. a SparseTimeValueModel used solely to provide faked Chris@691: // completion information to the AlignmentModel while a Chris@691: // TuningDifference calculation is going on. We call this Chris@691: // preparatoryModel. Chris@672: // Chris@691: // 3. an AlignmentModel, which stores the path and carries out Chris@691: // alignment lookups on it. We just call this one alignmentModel. Chris@672: // Chris@691: // Models 1 and 3 are registered with the document, which will Chris@691: // eventually release them. We don't release them here except in Chris@691: // the case where an activity fails before the point where we Chris@691: // would otherwise have registered them with the document. Chris@691: // Chris@691: // Models 2a (tuningDiffOutputModel), 2b (pathOutputModel) and 2c Chris@691: // (preparatoryModel) are not registered with the document. Model Chris@691: // 2b (pathOutputModel) is not registered because we do not have a Chris@691: // stable reference to the document at the point where it is Chris@691: // created. Model 2c (preparatoryModel) is not registered because Chris@691: // it is a bodge that we are embarrassed about, so we try to Chris@691: // manage it ourselves without anyone else noticing. Model 2a is Chris@691: // not registered for symmetry with the other two. These have to Chris@691: // be released by us when finished with, but their lifespans do Chris@691: // not extend beyond the end of the alignment procedure, so this Chris@691: // should be ok. Chris@420: Chris@420: AggregateWaveModel::ChannelSpecList components; Chris@420: Chris@687: components.push_back Chris@687: (AggregateWaveModel::ModelChannelSpec(referenceId, -1)); Chris@420: Chris@687: components.push_back Chris@687: (AggregateWaveModel::ModelChannelSpec(otherId, -1)); Chris@420: Chris@683: auto aggregateModel = std::make_shared(components); Chris@687: auto aggregateModelId = ModelById::add(aggregateModel); Chris@691: doc->addNonDerivedModel(aggregateModelId); Chris@670: Chris@687: auto alignmentModel = std::make_shared Chris@687: (referenceId, otherId, ModelId()); Chris@687: auto alignmentModelId = ModelById::add(alignmentModel); Chris@670: Chris@670: TransformId tdId = getTuningDifferenceTransformName(); Chris@670: Chris@670: if (tdId == "") { Chris@670: Chris@687: if (beginTransformDrivenAlignment(aggregateModelId, Chris@687: alignmentModelId)) { Chris@687: other->setAlignment(alignmentModelId); Chris@691: doc->addNonDerivedModel(alignmentModelId); Chris@670: } else { Chris@670: error = alignmentModel->getError(); Chris@683: ModelById::release(alignmentModel); Chris@670: return false; Chris@670: } Chris@670: Chris@670: } else { Chris@670: Chris@670: // Have a tuning-difference transform id, so run it Chris@670: // asynchronously first Chris@670: Chris@670: TransformFactory *tf = TransformFactory::getInstance(); Chris@670: Chris@670: Transform transform = tf->getDefaultTransformFor Chris@670: (tdId, aggregateModel->getSampleRate()); Chris@670: Chris@678: transform.setParameter("maxduration", 60); Chris@678: transform.setParameter("maxrange", 6); Chris@678: transform.setParameter("finetuning", false); Chris@671: Chris@670: SVDEBUG << "Align::alignModel: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; Chris@670: Chris@670: ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); Chris@670: Chris@670: QString message; Chris@691: ModelId tuningDiffOutputModelId = mtf->transform(transform, Chris@691: aggregateModelId, Chris@691: message); Chris@670: Chris@691: auto tuningDiffOutputModel = Chris@691: ModelById::getAs(tuningDiffOutputModelId); Chris@691: if (!tuningDiffOutputModel) { Chris@670: SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl; Chris@670: error = message; Chris@691: ModelById::release(alignmentModel); Chris@670: return false; Chris@670: } Chris@670: Chris@687: other->setAlignment(alignmentModelId); Chris@691: doc->addNonDerivedModel(alignmentModelId); Chris@665: Chris@691: connect(tuningDiffOutputModel.get(), Chris@691: SIGNAL(completionChanged(ModelId)), Chris@687: this, SLOT(tuningDifferenceCompletionChanged(ModelId))); Chris@420: Chris@671: TuningDiffRec rec; Chris@687: rec.input = aggregateModelId; Chris@687: rec.alignment = alignmentModelId; Chris@677: Chris@671: // This model exists only so that the AlignmentModel can get a Chris@671: // completion value from somewhere while the tuning difference Chris@671: // calculation is going on Chris@683: auto preparatoryModel = std::make_shared Chris@683: (aggregateModel->getSampleRate(), 1); Chris@687: auto preparatoryModelId = ModelById::add(preparatoryModel); Chris@683: preparatoryModel->setCompletion(0); Chris@687: rec.preparatory = preparatoryModelId; Chris@671: alignmentModel->setPathFrom(rec.preparatory); Chris@671: Chris@691: m_pendingTuningDiffs[tuningDiffOutputModelId] = rec; Chris@698: Chris@698: SVDEBUG << "Align::alignModelViaTransform: Made a note of pending tuning diff output model id " << tuningDiffOutputModelId << " with input " << rec.input << ", alignment model " << rec.alignment << ", preparatory model " << rec.preparatory << endl; Chris@670: } Chris@670: Chris@670: return true; Chris@670: } Chris@670: Chris@671: void Chris@691: Align::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId) Chris@671: { Chris@687: QMutexLocker locker(&m_mutex); Chris@671: Chris@691: if (m_pendingTuningDiffs.find(tuningDiffOutputModelId) == Chris@691: m_pendingTuningDiffs.end()) { Chris@698: SVDEBUG << "NOTE: Align::tuningDifferenceCompletionChanged: Model " Chris@698: << tuningDiffOutputModelId Chris@698: << " not found in pending tuning diff map, probably " Chris@698: << "completed already" << endl; Chris@683: return; Chris@683: } Chris@671: Chris@691: auto tuningDiffOutputModel = Chris@691: ModelById::getAs(tuningDiffOutputModelId); Chris@691: if (!tuningDiffOutputModel) { Chris@683: SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged: Model " Chris@691: << tuningDiffOutputModelId Chris@691: << " not known as SparseTimeValueModel" << endl; Chris@683: return; Chris@683: } Chris@683: Chris@691: TuningDiffRec rec = m_pendingTuningDiffs[tuningDiffOutputModelId]; Chris@683: Chris@691: auto alignmentModel = ModelById::getAs(rec.alignment); Chris@691: if (!alignmentModel) { Chris@683: SVCERR << "WARNING: Align::tuningDifferenceCompletionChanged:" Chris@683: << "alignment model has disappeared" << endl; Chris@683: return; Chris@683: } Chris@683: Chris@671: int completion = 0; Chris@691: bool done = tuningDiffOutputModel->isReady(&completion); Chris@671: Chris@671: if (!done) { Chris@671: // This will be the completion the alignment model reports, Chris@671: // before the alignment actually begins. It goes up from 0 to Chris@671: // 99 (not 100!) and then back to 0 again when we start Chris@671: // calculating the actual path in the following phase Chris@671: int clamped = (completion == 100 ? 99 : completion); Chris@691: auto preparatoryModel = Chris@691: ModelById::getAs(rec.preparatory); Chris@691: if (preparatoryModel) { Chris@691: preparatoryModel->setCompletion(clamped); Chris@683: } Chris@671: return; Chris@671: } Chris@671: Chris@671: float tuningFrequency = 440.f; Chris@671: Chris@691: if (!tuningDiffOutputModel->isEmpty()) { Chris@691: tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue(); Chris@671: SVCERR << "Align::tuningDifferenceCompletionChanged: Reported tuning frequency = " << tuningFrequency << endl; Chris@671: } else { Chris@671: SVCERR << "Align::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl; Chris@671: } Chris@698: Chris@691: ModelById::release(tuningDiffOutputModel); Chris@683: Chris@691: alignmentModel->setPathFrom({}); // replace preparatoryModel Chris@691: ModelById::release(rec.preparatory); Chris@691: rec.preparatory = {}; Chris@691: Chris@691: m_pendingTuningDiffs.erase(tuningDiffOutputModelId); Chris@698: Chris@698: SVDEBUG << "Align::tuningDifferenceCompletionChanged: Erasing model " Chris@698: << tuningDiffOutputModelId << " from pending tuning diffs and " Chris@698: << "launching the alignment phase for alignment model " Chris@698: << rec.alignment << " with tuning frequency " Chris@698: << tuningFrequency << endl; Chris@671: Chris@671: beginTransformDrivenAlignment Chris@671: (rec.input, rec.alignment, tuningFrequency); Chris@671: } Chris@671: Chris@670: bool Chris@683: Align::beginTransformDrivenAlignment(ModelId aggregateModelId, Chris@683: ModelId alignmentModelId, Chris@670: float tuningFrequency) Chris@670: { Chris@428: TransformId id = getAlignmentTransformName(); Chris@420: Chris@420: TransformFactory *tf = TransformFactory::getInstance(); Chris@420: Chris@691: auto aggregateModel = Chris@691: ModelById::getAs(aggregateModelId); Chris@691: auto alignmentModel = Chris@691: ModelById::getAs(alignmentModelId); Chris@683: Chris@683: if (!aggregateModel || !alignmentModel) { Chris@683: SVCERR << "Align::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl; Chris@683: return false; Chris@683: } Chris@683: Chris@420: Transform transform = tf->getDefaultTransformFor Chris@420: (id, aggregateModel->getSampleRate()); Chris@420: Chris@420: transform.setStepSize(transform.getBlockSize()/2); Chris@420: transform.setParameter("serialise", 1); Chris@420: transform.setParameter("smooth", 0); Chris@699: transform.setParameter("zonewidth", 40); Chris@701: // transform.setParameter("noise", true); Chris@699: transform.setParameter("minfreq", 250); Chris@701: // transform.setParameter("usechroma", 1); Chris@420: Chris@670: if (tuningFrequency != 0.f) { Chris@670: transform.setParameter("freq2", tuningFrequency); Chris@670: } Chris@670: Chris@420: SVDEBUG << "Align::alignModel: Alignment transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; Chris@420: Chris@420: ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); Chris@420: Chris@420: QString message; Chris@691: ModelId pathOutputModelId = mtf->transform Chris@683: (transform, aggregateModelId, message); Chris@420: Chris@691: if (pathOutputModelId.isNone()) { Chris@420: transform.setStepSize(0); Chris@691: pathOutputModelId = mtf->transform Chris@683: (transform, aggregateModelId, message); Chris@420: } Chris@420: Chris@691: auto pathOutputModel = Chris@691: ModelById::getAs(pathOutputModelId); Chris@420: Chris@670: //!!! callers will need to be updated to get error from Chris@670: //!!! alignment model after initial call Chris@670: Chris@691: if (!pathOutputModel) { Chris@649: SVCERR << "Align::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl; Chris@670: alignmentModel->setError(message); Chris@420: return false; Chris@420: } Chris@420: Chris@691: pathOutputModel->setCompletion(0); Chris@691: alignmentModel->setPathFrom(pathOutputModelId); Chris@691: Chris@691: m_pendingAlignments[alignmentModelId] = pathOutputModelId; Chris@420: Chris@687: connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)), Chris@687: this, SLOT(alignmentCompletionChanged(ModelId))); Chris@420: Chris@420: return true; Chris@420: } Chris@420: Chris@428: void Chris@691: Align::alignmentCompletionChanged(ModelId alignmentModelId) Chris@428: { Chris@670: QMutexLocker locker (&m_mutex); Chris@683: Chris@691: auto alignmentModel = ModelById::getAs(alignmentModelId); Chris@691: Chris@691: if (alignmentModel && alignmentModel->isReady()) { Chris@691: Chris@691: if (m_pendingAlignments.find(alignmentModelId) != Chris@691: m_pendingAlignments.end()) { Chris@691: ModelId pathOutputModelId = m_pendingAlignments[alignmentModelId]; Chris@691: ModelById::release(pathOutputModelId); Chris@691: m_pendingAlignments.erase(alignmentModelId); Chris@691: } Chris@691: Chris@691: disconnect(alignmentModel.get(), Chris@691: SIGNAL(completionChanged(ModelId)), Chris@687: this, SLOT(alignmentCompletionChanged(ModelId))); Chris@691: emit alignmentComplete(alignmentModelId); Chris@428: } Chris@428: } Chris@428: Chris@420: bool Chris@691: Align::alignModelViaProgram(Document *doc, Chris@687: ModelId referenceId, Chris@687: ModelId otherId, Chris@687: QString program, Chris@687: QString &error) Chris@420: { Chris@670: QMutexLocker locker (&m_mutex); Chris@430: Chris@420: // Run an external program, passing to it paths to the main Chris@420: // model's audio file and the new model's audio file. It returns Chris@420: // the path in CSV form through stdout. Chris@420: Chris@687: auto reference = ModelById::getAs(referenceId); Chris@687: auto other = ModelById::getAs(otherId); Chris@687: if (!reference || !other) { Chris@649: SVCERR << "ERROR: Align::alignModelViaProgram: Can't align non-read-only models via program (no local filename available)" << endl; Chris@515: return false; Chris@515: } Chris@687: Chris@687: while (!reference->isReady(nullptr) || !other->isReady(nullptr)) { Chris@687: qApp->processEvents(); Chris@687: } Chris@515: Chris@687: QString refPath = reference->getLocalFilename(); Chris@687: QString otherPath = other->getLocalFilename(); Chris@420: Chris@420: if (refPath == "" || otherPath == "") { Chris@670: error = "Failed to find local filepath for wave-file model"; Chris@595: return false; Chris@420: } Chris@420: Chris@687: auto alignmentModel = Chris@687: std::make_shared(referenceId, otherId, ModelId()); Chris@687: auto alignmentModelId = ModelById::add(alignmentModel); Chris@687: other->setAlignment(alignmentModelId); Chris@423: Chris@423: QProcess *process = new QProcess; Chris@420: QStringList args; Chris@420: args << refPath << otherPath; Chris@423: Chris@423: connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), Chris@423: this, SLOT(alignmentProgramFinished(int, QProcess::ExitStatus))); Chris@420: Chris@687: m_pendingProcesses[process] = alignmentModelId; Chris@423: process->start(program, args); Chris@420: Chris@423: bool success = process->waitForStarted(); Chris@423: Chris@423: if (!success) { Chris@649: SVCERR << "ERROR: Align::alignModelViaProgram: Program did not start" Chris@649: << endl; Chris@670: error = "Alignment program could not be started"; Chris@670: m_pendingProcesses.erase(process); Chris@687: other->setAlignment({}); Chris@691: ModelById::release(alignmentModelId); Chris@423: delete process; Chris@423: } Chris@423: Chris@691: doc->addNonDerivedModel(alignmentModelId); Chris@423: return success; Chris@423: } Chris@423: Chris@423: void Chris@423: Align::alignmentProgramFinished(int exitCode, QProcess::ExitStatus status) Chris@423: { Chris@670: QMutexLocker locker (&m_mutex); Chris@670: Chris@649: SVCERR << "Align::alignmentProgramFinished" << endl; Chris@423: Chris@423: QProcess *process = qobject_cast(sender()); Chris@423: Chris@670: if (m_pendingProcesses.find(process) == m_pendingProcesses.end()) { Chris@649: SVCERR << "ERROR: Align::alignmentProgramFinished: Process " << process Chris@649: << " not found in process model map!" << endl; Chris@423: return; Chris@423: } Chris@423: Chris@683: ModelId alignmentModelId = m_pendingProcesses[process]; Chris@683: auto alignmentModel = ModelById::getAs(alignmentModelId); Chris@683: if (!alignmentModel) return; Chris@423: Chris@423: if (exitCode == 0 && status == 0) { Chris@420: Chris@595: CSVFormat format; Chris@595: format.setModelType(CSVFormat::TwoDimensionalModel); Chris@595: format.setTimingType(CSVFormat::ExplicitTiming); Chris@595: format.setTimeUnits(CSVFormat::TimeSeconds); Chris@595: format.setColumnCount(2); Chris@425: // The output format has time in the reference file first, and Chris@425: // time in the "other" file in the second column. This is a Chris@425: // more natural approach for a command-line alignment tool, Chris@425: // but it's the opposite of what we expect for native Chris@425: // alignment paths, which map from "other" file to Chris@425: // reference. These column purpose settings reflect that. Chris@595: format.setColumnPurpose(1, CSVFormat::ColumnStartTime); Chris@595: format.setColumnPurpose(0, CSVFormat::ColumnValue); Chris@595: format.setAllowQuoting(false); Chris@595: format.setSeparator(','); Chris@420: Chris@595: CSVFileReader reader(process, format, alignmentModel->getSampleRate()); Chris@595: if (!reader.isOK()) { Chris@649: SVCERR << "ERROR: Align::alignmentProgramFinished: Failed to parse output" Chris@649: << endl; Chris@670: alignmentModel->setError Chris@670: (QString("Failed to parse output of program: %1") Chris@670: .arg(reader.getError())); Chris@423: goto done; Chris@595: } Chris@420: Chris@683: //!!! to use ById? Chris@683: Chris@595: Model *csvOutput = reader.load(); Chris@420: Chris@687: SparseTimeValueModel *path = Chris@687: qobject_cast(csvOutput); Chris@595: if (!path) { Chris@649: SVCERR << "ERROR: Align::alignmentProgramFinished: Output did not convert to sparse time-value model" Chris@649: << endl; Chris@670: alignmentModel->setError Chris@670: ("Output of program did not produce sparse time-value model"); Chris@683: delete csvOutput; Chris@423: goto done; Chris@595: } Chris@683: Chris@649: if (path->isEmpty()) { Chris@649: SVCERR << "ERROR: Align::alignmentProgramFinished: Output contained no mappings" Chris@649: << endl; Chris@670: alignmentModel->setError Chris@670: ("Output of alignment program contained no mappings"); Chris@683: delete path; Chris@423: goto done; Chris@595: } Chris@420: Chris@649: SVCERR << "Align::alignmentProgramFinished: Setting alignment path (" Chris@650: << path->getEventCount() << " point(s))" << endl; Chris@650: Chris@687: auto pathId = Chris@687: ModelById::add(std::shared_ptr(path)); Chris@687: alignmentModel->setPathFrom(pathId); Chris@420: Chris@683: emit alignmentComplete(alignmentModelId); Chris@687: Chris@687: ModelById::release(pathId); Chris@428: Chris@420: } else { Chris@649: SVCERR << "ERROR: Align::alignmentProgramFinished: Aligner program " Chris@649: << "failed: exit code " << exitCode << ", status " << status Chris@649: << endl; Chris@670: alignmentModel->setError Chris@670: ("Aligner process returned non-zero exit status"); Chris@420: } Chris@420: Chris@423: done: Chris@670: m_pendingProcesses.erase(process); Chris@423: delete process; Chris@420: } Chris@420: