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@777: #include "MATCHAligner.h" Chris@752: Chris@752: #include "data/model/SparseTimeValueModel.h" Chris@752: #include "data/model/RangeSummarisableTimeValueModel.h" Chris@752: #include "data/model/AlignmentModel.h" Chris@752: #include "data/model/AggregateWaveModel.h" Chris@752: Chris@752: #include "framework/Document.h" Chris@752: Chris@752: #include "transform/TransformFactory.h" Chris@752: #include "transform/ModelTransformerFactory.h" Chris@752: #include "transform/FeatureExtractionModelTransformer.h" Chris@752: Chris@752: #include Chris@752: Chris@777: MATCHAligner::MATCHAligner(Document *doc, Chris@777: ModelId reference, Chris@777: ModelId toAlign, Chris@781: bool subsequence, Chris@777: bool withTuningDifference) : Chris@752: m_document(doc), Chris@752: m_reference(reference), Chris@752: m_toAlign(toAlign), Chris@781: m_subsequence(subsequence), Chris@767: m_withTuningDifference(withTuningDifference), Chris@752: m_tuningFrequency(440.f), Chris@752: m_incomplete(true) Chris@752: { Chris@752: } Chris@752: Chris@777: MATCHAligner::~MATCHAligner() Chris@752: { Chris@752: if (m_incomplete) { Chris@752: auto other = Chris@752: ModelById::getAs(m_toAlign); Chris@752: if (other) { Chris@752: other->setAlignment({}); Chris@752: } Chris@752: } Chris@752: Chris@752: ModelById::release(m_tuningDiffOutputModel); Chris@752: ModelById::release(m_pathOutputModel); Chris@752: } Chris@752: Chris@752: QString Chris@781: MATCHAligner::getAlignmentTransformName(bool subsequence) Chris@752: { Chris@752: QSettings settings; Chris@752: settings.beginGroup("Alignment"); Chris@781: TransformId id; Chris@781: if (subsequence) { Chris@781: id = settings.value Chris@781: ("transform-id-subsequence", Chris@781: "vamp:match-vamp-plugin:match-subsequence:path").toString(); Chris@781: } else { Chris@781: id = settings.value Chris@781: ("transform-id", Chris@781: "vamp:match-vamp-plugin:match:path").toString(); Chris@781: } Chris@752: settings.endGroup(); Chris@752: return id; Chris@752: } Chris@752: Chris@752: QString Chris@777: MATCHAligner::getTuningDifferenceTransformName() Chris@752: { Chris@752: QSettings settings; Chris@752: settings.beginGroup("Alignment"); Chris@767: TransformId id = settings.value Chris@767: ("tuning-difference-transform-id", Chris@767: "vamp:tuning-difference:tuning-difference:tuningfreq") Chris@767: .toString(); Chris@752: settings.endGroup(); Chris@752: return id; Chris@752: } Chris@752: Chris@752: bool Chris@777: MATCHAligner::isAvailable() Chris@752: { Chris@752: TransformFactory *factory = TransformFactory::getInstance(); Chris@781: TransformId id = getAlignmentTransformName(false); Chris@781: TransformId subId = getAlignmentTransformName(true); Chris@752: TransformId tdId = getTuningDifferenceTransformName(); Chris@752: return factory->haveTransform(id) && Chris@781: (subId == "" || factory->haveTransform(subId)) && Chris@752: (tdId == "" || factory->haveTransform(tdId)); Chris@752: } Chris@752: Chris@761: void Chris@777: MATCHAligner::begin() Chris@752: { Chris@752: auto reference = Chris@752: ModelById::getAs(m_reference); Chris@752: auto other = Chris@752: ModelById::getAs(m_toAlign); Chris@752: Chris@761: if (!reference || !other) return; Chris@752: Chris@752: // This involves creating a number of new models: Chris@752: // Chris@752: // 1. an AggregateWaveModel to provide the mixdowns of the main Chris@752: // model and the new model in its two channels, as input to the Chris@752: // MATCH plugin. We just call this one aggregateModel Chris@752: // Chris@752: // 2a. a SparseTimeValueModel which will be automatically created Chris@752: // by FeatureExtractionModelTransformer when running the Chris@752: // TuningDifference plugin to receive the relative tuning of the Chris@752: // second model (if pitch-aware alignment is enabled in the Chris@752: // preferences). This is m_tuningDiffOutputModel. Chris@752: // Chris@752: // 2b. a SparseTimeValueModel which will be automatically created Chris@752: // by FeatureExtractionPluginTransformer when running the MATCH Chris@752: // plugin to perform alignment (so containing the alignment path). Chris@752: // This is m_pathOutputModel. Chris@752: // Chris@752: // 3. an AlignmentModel, which stores the path and carries out Chris@752: // alignment lookups on it. This one is m_alignmentModel. Chris@752: // Chris@752: // Models 1 and 3 are registered with the document, which will Chris@752: // eventually release them. We don't release them here except in Chris@752: // the case where an activity fails before the point where we Chris@752: // would otherwise have registered them with the document. Chris@752: // Chris@768: // Models 2a (m_tuningDiffOutputModel) and 2b (m_pathOutputModel) Chris@768: // are not registered with the document, because they are not Chris@768: // intended to persist. These have to be released by us when Chris@752: // finished with, but their lifespans do not extend beyond the end Chris@752: // of the alignment procedure, so this should be ok. Chris@752: Chris@752: AggregateWaveModel::ChannelSpecList components; Chris@752: components.push_back Chris@752: (AggregateWaveModel::ModelChannelSpec(m_reference, -1)); Chris@752: Chris@752: components.push_back Chris@752: (AggregateWaveModel::ModelChannelSpec(m_toAlign, -1)); Chris@752: Chris@752: auto aggregateModel = std::make_shared(components); Chris@752: m_aggregateModel = ModelById::add(aggregateModel); Chris@752: m_document->addNonDerivedModel(m_aggregateModel); Chris@752: Chris@752: auto alignmentModel = std::make_shared Chris@752: (m_reference, m_toAlign, ModelId()); Chris@752: m_alignmentModel = ModelById::add(alignmentModel); Chris@752: Chris@767: TransformId tdId; Chris@767: if (m_withTuningDifference) { Chris@767: tdId = getTuningDifferenceTransformName(); Chris@767: } Chris@752: Chris@752: if (tdId == "") { Chris@752: Chris@752: if (beginAlignmentPhase()) { Chris@752: other->setAlignment(m_alignmentModel); Chris@752: m_document->addNonDerivedModel(m_alignmentModel); Chris@752: } else { Chris@761: QString error = alignmentModel->getError(); Chris@752: ModelById::release(alignmentModel); Chris@761: emit failed(m_toAlign, error); Chris@761: return; Chris@752: } Chris@752: Chris@752: } else { Chris@752: Chris@752: // Have a tuning-difference transform id, so run it Chris@752: // asynchronously first Chris@752: Chris@752: TransformFactory *tf = TransformFactory::getInstance(); Chris@752: Chris@752: Transform transform = tf->getDefaultTransformFor Chris@752: (tdId, aggregateModel->getSampleRate()); Chris@752: Chris@752: transform.setParameter("maxduration", 60); Chris@752: transform.setParameter("maxrange", 6); Chris@752: transform.setParameter("finetuning", false); Chris@752: Chris@777: SVDEBUG << "MATCHAligner: Tuning difference transform step size " << transform.getStepSize() << ", block size " << transform.getBlockSize() << endl; Chris@752: Chris@752: ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); Chris@752: Chris@752: QString message; Chris@753: m_tuningDiffOutputModel = mtf->transform(transform, Chris@753: m_aggregateModel, Chris@753: message); Chris@752: Chris@752: auto tuningDiffOutputModel = Chris@753: ModelById::getAs(m_tuningDiffOutputModel); Chris@752: if (!tuningDiffOutputModel) { Chris@752: SVCERR << "Align::alignModel: ERROR: Failed to create tuning-difference output model (no Tuning Difference plugin?)" << endl; Chris@752: ModelById::release(alignmentModel); Chris@761: emit failed(m_toAlign, message); Chris@761: return; Chris@752: } Chris@752: Chris@752: other->setAlignment(m_alignmentModel); Chris@752: m_document->addNonDerivedModel(m_alignmentModel); Chris@752: Chris@752: connect(tuningDiffOutputModel.get(), Chris@752: SIGNAL(completionChanged(ModelId)), Chris@752: this, SLOT(tuningDifferenceCompletionChanged(ModelId))); Chris@752: } Chris@752: } Chris@752: Chris@752: void Chris@777: MATCHAligner::tuningDifferenceCompletionChanged(ModelId tuningDiffOutputModelId) Chris@752: { Chris@761: if (m_tuningDiffOutputModel.isNone()) { Chris@761: // we're done, this is probably a spurious queued event Chris@761: return; Chris@761: } Chris@761: Chris@752: if (tuningDiffOutputModelId != m_tuningDiffOutputModel) { Chris@777: SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged: Model " Chris@753: << tuningDiffOutputModelId Chris@753: << " is not ours! (ours is " Chris@753: << m_tuningDiffOutputModel << ")" << endl; Chris@752: return; Chris@752: } Chris@752: Chris@752: auto tuningDiffOutputModel = Chris@752: ModelById::getAs(m_tuningDiffOutputModel); Chris@752: if (!tuningDiffOutputModel) { Chris@777: SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged: Model " Chris@752: << tuningDiffOutputModelId Chris@752: << " not known as SparseTimeValueModel" << endl; Chris@752: return; Chris@752: } Chris@752: Chris@752: auto alignmentModel = ModelById::getAs(m_alignmentModel); Chris@752: if (!alignmentModel) { Chris@777: SVCERR << "WARNING: MATCHAligner::tuningDifferenceCompletionChanged:" Chris@752: << "alignment model has disappeared" << endl; Chris@752: return; Chris@752: } Chris@752: Chris@752: int completion = 0; Chris@752: bool done = tuningDiffOutputModel->isReady(&completion); Chris@752: Chris@777: SVDEBUG << "MATCHAligner::tuningDifferenceCompletionChanged: model " Chris@761: << m_tuningDiffOutputModel << ", completion = " << completion Chris@761: << ", done = " << done << endl; Chris@761: Chris@752: if (!done) { Chris@752: // This will be the completion the alignment model reports, Chris@769: // before the alignment actually begins Chris@769: alignmentModel->setCompletion(completion / 2); Chris@752: return; Chris@752: } Chris@752: Chris@752: m_tuningFrequency = 440.f; Chris@752: Chris@752: if (!tuningDiffOutputModel->isEmpty()) { Chris@752: m_tuningFrequency = tuningDiffOutputModel->getAllEvents()[0].getValue(); Chris@777: SVCERR << "MATCHAligner::tuningDifferenceCompletionChanged: Reported tuning frequency = " << m_tuningFrequency << endl; Chris@752: } else { Chris@777: SVCERR << "MATCHAligner::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl; Chris@752: } Chris@752: Chris@752: ModelById::release(tuningDiffOutputModel); Chris@752: m_tuningDiffOutputModel = {}; Chris@752: Chris@752: beginAlignmentPhase(); Chris@752: } Chris@752: Chris@752: bool Chris@777: MATCHAligner::beginAlignmentPhase() Chris@752: { Chris@781: TransformId id = getAlignmentTransformName(m_subsequence); Chris@752: Chris@777: SVDEBUG << "MATCHAligner::beginAlignmentPhase: transform is " Chris@753: << id << endl; Chris@753: Chris@752: TransformFactory *tf = TransformFactory::getInstance(); Chris@752: Chris@752: auto aggregateModel = Chris@752: ModelById::getAs(m_aggregateModel); Chris@752: auto alignmentModel = Chris@752: ModelById::getAs(m_alignmentModel); Chris@752: Chris@752: if (!aggregateModel || !alignmentModel) { Chris@777: SVCERR << "MATCHAligner::alignModel: ERROR: One or other of the aggregate & alignment models has disappeared" << endl; Chris@752: return false; Chris@752: } Chris@752: Chris@752: Transform transform = tf->getDefaultTransformFor Chris@752: (id, aggregateModel->getSampleRate()); Chris@752: Chris@752: transform.setStepSize(transform.getBlockSize()/2); Chris@752: transform.setParameter("serialise", 1); Chris@752: transform.setParameter("smooth", 0); Chris@752: transform.setParameter("zonewidth", 40); Chris@752: transform.setParameter("noise", true); Chris@752: transform.setParameter("minfreq", 500); Chris@752: Chris@752: int cents = 0; Chris@752: Chris@752: if (m_tuningFrequency != 0.f) { Chris@752: transform.setParameter("freq2", m_tuningFrequency); Chris@752: Chris@752: double centsOffset = 0.f; Chris@752: int pitch = Pitch::getPitchForFrequency(m_tuningFrequency, Chris@752: ¢sOffset); Chris@752: cents = int(round((pitch - 69) * 100 + centsOffset)); Chris@777: SVCERR << "MATCHAligner: frequency " << m_tuningFrequency Chris@752: << " yields cents offset " << centsOffset Chris@752: << " and pitch " << pitch << " -> cents " << cents << endl; Chris@752: } Chris@752: Chris@752: alignmentModel->setRelativePitch(cents); Chris@752: Chris@777: SVDEBUG << "MATCHAligner: Alignment transform step size " Chris@752: << transform.getStepSize() << ", block size " Chris@752: << transform.getBlockSize() << endl; Chris@752: Chris@752: ModelTransformerFactory *mtf = ModelTransformerFactory::getInstance(); Chris@752: Chris@752: QString message; Chris@753: m_pathOutputModel = mtf->transform Chris@752: (transform, m_aggregateModel, message); Chris@752: Chris@753: if (m_pathOutputModel.isNone()) { Chris@752: transform.setStepSize(0); Chris@753: m_pathOutputModel = mtf->transform Chris@752: (transform, m_aggregateModel, message); Chris@752: } Chris@752: Chris@752: auto pathOutputModel = Chris@753: ModelById::getAs(m_pathOutputModel); Chris@752: Chris@752: //!!! callers will need to be updated to get error from Chris@752: //!!! alignment model after initial call Chris@752: Chris@752: if (!pathOutputModel) { Chris@777: SVCERR << "MATCHAligner: ERROR: Failed to create alignment path (no MATCH plugin?)" << endl; Chris@752: alignmentModel->setError(message); Chris@752: return false; Chris@752: } Chris@752: Chris@752: pathOutputModel->setCompletion(0); Chris@752: alignmentModel->setPathFrom(m_pathOutputModel); Chris@752: Chris@769: connect(pathOutputModel.get(), SIGNAL(completionChanged(ModelId)), Chris@752: this, SLOT(alignmentCompletionChanged(ModelId))); Chris@752: Chris@752: return true; Chris@752: } Chris@752: Chris@752: void Chris@777: MATCHAligner::alignmentCompletionChanged(ModelId pathOutputModelId) Chris@752: { Chris@769: if (pathOutputModelId != m_pathOutputModel) { Chris@777: SVCERR << "WARNING: MATCHAligner::alignmentCompletionChanged: Model " Chris@769: << pathOutputModelId Chris@753: << " is not ours! (ours is " Chris@769: << m_pathOutputModel << ")" << endl; Chris@752: return; Chris@752: } Chris@752: Chris@769: auto pathOutputModel = Chris@769: ModelById::getAs(m_pathOutputModel); Chris@769: if (!pathOutputModel) { Chris@777: SVCERR << "WARNING: MATCHAligner::alignmentCompletionChanged: Path output model " Chris@769: << m_pathOutputModel << " no longer exists" << endl; Chris@769: return; Chris@769: } Chris@769: Chris@769: int completion = 0; Chris@769: bool done = pathOutputModel->isReady(&completion); Chris@752: Chris@769: if (m_withTuningDifference) { Chris@769: if (auto alignmentModel = Chris@769: ModelById::getAs(m_alignmentModel)) { Chris@769: if (!done) { Chris@769: int adjustedCompletion = 50 + completion/2; Chris@769: if (adjustedCompletion > 99) { Chris@769: adjustedCompletion = 99; Chris@769: } Chris@769: alignmentModel->setCompletion(adjustedCompletion); Chris@769: } else { Chris@769: alignmentModel->setCompletion(100); Chris@769: } Chris@769: } Chris@769: } Chris@752: Chris@769: if (done) { Chris@752: m_incomplete = false; Chris@752: Chris@752: ModelById::release(m_pathOutputModel); Chris@752: m_pathOutputModel = {}; Chris@752: Chris@752: emit complete(m_alignmentModel); Chris@752: } Chris@752: }