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 "TransformAligner.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@752: TransformAligner::TransformAligner(Document *doc, Chris@752: ModelId reference, Chris@752: ModelId toAlign) : Chris@752: m_document(doc), Chris@752: m_reference(reference), Chris@752: m_toAlign(toAlign), Chris@752: m_tuningFrequency(440.f), Chris@752: m_incomplete(true) Chris@752: { Chris@752: } Chris@752: Chris@752: TransformAligner::~TransformAligner() 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_tuningDiffProgressModel); Chris@752: ModelById::release(m_tuningDiffOutputModel); Chris@752: ModelById::release(m_pathOutputModel); Chris@752: } Chris@752: Chris@752: QString Chris@752: TransformAligner::getAlignmentTransformName() Chris@752: { Chris@752: QSettings settings; Chris@752: settings.beginGroup("Alignment"); Chris@752: TransformId id = Chris@752: settings.value("transform-id", Chris@752: "vamp:match-vamp-plugin:match:path").toString(); Chris@752: settings.endGroup(); Chris@752: return id; Chris@752: } Chris@752: Chris@752: QString Chris@752: TransformAligner::getTuningDifferenceTransformName() Chris@752: { Chris@752: QSettings settings; Chris@752: settings.beginGroup("Alignment"); Chris@752: bool performPitchCompensation = Chris@752: settings.value("align-pitch-aware", false).toBool(); Chris@752: QString id = ""; Chris@752: if (performPitchCompensation) { Chris@752: id = settings.value Chris@752: ("tuning-difference-transform-id", Chris@752: "vamp:tuning-difference:tuning-difference:tuningfreq") Chris@752: .toString(); Chris@752: } Chris@752: settings.endGroup(); Chris@752: return id; Chris@752: } Chris@752: Chris@752: bool Chris@752: TransformAligner::isAvailable() Chris@752: { Chris@752: TransformFactory *factory = TransformFactory::getInstance(); Chris@752: TransformId id = getAlignmentTransformName(); Chris@752: TransformId tdId = getTuningDifferenceTransformName(); Chris@752: return factory->haveTransform(id) && Chris@752: (tdId == "" || factory->haveTransform(tdId)); Chris@752: } Chris@752: Chris@761: void Chris@761: TransformAligner::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: // 2c. a SparseTimeValueModel used solely to provide faked Chris@752: // completion information to the AlignmentModel while a Chris@752: // TuningDifference calculation is going on. We call this Chris@752: // m_tuningDiffProgressModel. 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@752: // Models 2a (m_tuningDiffOutputModel), 2b (m_pathOutputModel) and Chris@752: // 2c (m_tuningDiffProgressModel) are not registered with the Chris@752: // document, because they are not intended to persist, and also Chris@752: // Model 2c (m_tuningDiffProgressModel) is a bodge that we are Chris@752: // embarrassed about, so we try to manage it ourselves without Chris@752: // anyone else noticing. 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@752: TransformId tdId = getTuningDifferenceTransformName(); 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@752: SVDEBUG << "TransformAligner: 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: // This model exists only so that the AlignmentModel can get a Chris@752: // completion value from somewhere while the tuning difference Chris@752: // calculation is going on Chris@752: auto progressModel = std::make_shared Chris@752: (aggregateModel->getSampleRate(), 1); Chris@752: m_tuningDiffProgressModel = ModelById::add(progressModel); Chris@752: progressModel->setCompletion(0); Chris@752: alignmentModel->setPathFrom(m_tuningDiffProgressModel); Chris@752: } Chris@752: } Chris@752: Chris@752: void Chris@752: TransformAligner::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@752: SVCERR << "WARNING: TransformAligner::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@752: SVCERR << "WARNING: TransformAligner::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@752: SVCERR << "WARNING: TransformAligner::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@761: SVDEBUG << "TransformAligner::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@752: // before the alignment actually begins. It goes up from 0 to Chris@752: // 99 (not 100!) and then back to 0 again when we start Chris@752: // calculating the actual path in the following phase Chris@752: int clamped = (completion == 100 ? 99 : completion); Chris@752: auto progressModel = Chris@752: ModelById::getAs(m_tuningDiffProgressModel); Chris@752: if (progressModel) { Chris@752: progressModel->setCompletion(clamped); Chris@752: } 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@752: SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: Reported tuning frequency = " << m_tuningFrequency << endl; Chris@752: } else { Chris@752: SVCERR << "TransformAligner::tuningDifferenceCompletionChanged: No tuning frequency reported" << endl; Chris@752: } Chris@752: Chris@752: ModelById::release(tuningDiffOutputModel); Chris@752: m_tuningDiffOutputModel = {}; Chris@752: Chris@752: alignmentModel->setPathFrom({}); // replace m_tuningDiffProgressModel Chris@752: ModelById::release(m_tuningDiffProgressModel); Chris@752: m_tuningDiffProgressModel = {}; Chris@752: Chris@752: beginAlignmentPhase(); Chris@752: } Chris@752: Chris@752: bool Chris@752: TransformAligner::beginAlignmentPhase() Chris@752: { Chris@752: TransformId id = getAlignmentTransformName(); Chris@752: Chris@753: SVDEBUG << "TransformAligner::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@752: SVCERR << "TransformAligner::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@752: SVCERR << "TransformAligner: 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@753: SVDEBUG << "TransformAligner: 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@752: SVCERR << "TransformAligner: 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@752: connect(alignmentModel.get(), SIGNAL(completionChanged(ModelId)), Chris@752: this, SLOT(alignmentCompletionChanged(ModelId))); Chris@752: Chris@752: return true; Chris@752: } Chris@752: Chris@752: void Chris@752: TransformAligner::alignmentCompletionChanged(ModelId alignmentModelId) Chris@752: { Chris@752: if (alignmentModelId != m_alignmentModel) { Chris@752: SVCERR << "WARNING: TransformAligner::alignmentCompletionChanged: Model " Chris@753: << alignmentModelId Chris@753: << " is not ours! (ours is " Chris@753: << m_alignmentModel << ")" << endl; Chris@752: return; Chris@752: } Chris@752: Chris@752: auto alignmentModel = ModelById::getAs(m_alignmentModel); Chris@752: Chris@752: if (alignmentModel && alignmentModel->isReady()) { Chris@752: Chris@752: m_incomplete = false; Chris@752: Chris@752: ModelById::release(m_pathOutputModel); Chris@752: m_pathOutputModel = {}; Chris@752: Chris@752: disconnect(alignmentModel.get(), Chris@752: SIGNAL(completionChanged(ModelId)), Chris@752: this, SLOT(alignmentCompletionChanged(ModelId))); Chris@752: emit complete(m_alignmentModel); Chris@752: } Chris@752: }