Mercurial > hg > tuning-difference
diff src/TuningDifference.cpp @ 21:d660db57e902
Rearrange code, include subrepo etc
author | Chris Cannam |
---|---|
date | Thu, 05 Feb 2015 10:13:31 +0000 |
parents | chroma-compare-plugin/TuningDifference.cpp@331a520cdadb |
children | 6a75d371938f |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/TuningDifference.cpp Thu Feb 05 10:13:31 2015 +0000 @@ -0,0 +1,492 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + 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 "TuningDifference.h" + +#include <iostream> + +#include <cmath> +#include <cstdio> + +#include <algorithm> + +using namespace std; + +static double pitchToFrequency(int pitch, + double centsOffset = 0., + double concertA = 440.) +{ + double p = double(pitch) + (centsOffset / 100.); + return concertA * pow(2.0, (p - 69.0) / 12.0); +} + +static double frequencyForCentsAbove440(double cents) +{ + return pitchToFrequency(69, cents, 440.); +} + +TuningDifference::TuningDifference(float inputSampleRate) : + Plugin(inputSampleRate), + m_bpo(60), + m_refChroma(new Chromagram(paramsForTuningFrequency(440.))), + m_blockSize(0), + m_frameCount(0) +{ +} + +TuningDifference::~TuningDifference() +{ +} + +string +TuningDifference::getIdentifier() const +{ + return "tuning-difference"; +} + +string +TuningDifference::getName() const +{ + return "Tuning Difference"; +} + +string +TuningDifference::getDescription() const +{ + // Return something helpful here! + return ""; +} + +string +TuningDifference::getMaker() const +{ + // Your name here + return ""; +} + +int +TuningDifference::getPluginVersion() const +{ + // Increment this each time you release a version that behaves + // differently from the previous one + return 1; +} + +string +TuningDifference::getCopyright() const +{ + // This function is not ideally named. It does not necessarily + // need to say who made the plugin -- getMaker does that -- but it + // should indicate the terms under which it is distributed. For + // example, "Copyright (year). All Rights Reserved", or "GPL" + return ""; +} + +TuningDifference::InputDomain +TuningDifference::getInputDomain() const +{ + return TimeDomain; +} + +size_t +TuningDifference::getPreferredBlockSize() const +{ + return 0; +} + +size_t +TuningDifference::getPreferredStepSize() const +{ + return 0; +} + +size_t +TuningDifference::getMinChannelCount() const +{ + return 2; +} + +size_t +TuningDifference::getMaxChannelCount() const +{ + return 2; +} + +TuningDifference::ParameterList +TuningDifference::getParameterDescriptors() const +{ + ParameterList list; + //!!! parameter: max search range + //!!! parameter: fine search precision + return list; +} + +float +TuningDifference::getParameter(string) const +{ + return 0; +} + +void +TuningDifference::setParameter(string, float) +{ +} + +TuningDifference::ProgramList +TuningDifference::getPrograms() const +{ + ProgramList list; + return list; +} + +string +TuningDifference::getCurrentProgram() const +{ + return ""; // no programs +} + +void +TuningDifference::selectProgram(string) +{ +} + +TuningDifference::OutputList +TuningDifference::getOutputDescriptors() const +{ + OutputList list; + + OutputDescriptor d; + d.identifier = "cents"; + d.name = "Tuning Difference"; + d.description = "Difference in averaged frequency profile between channels 1 and 2, in cents. A positive value means channel 2 is higher."; + d.unit = "cents"; + d.hasFixedBinCount = true; + d.binCount = 1; + d.hasKnownExtents = false; + d.isQuantized = false; + d.sampleType = OutputDescriptor::VariableSampleRate; + d.hasDuration = false; + m_outputs[d.identifier] = list.size(); + list.push_back(d); + + d.identifier = "tuningfreq"; + d.name = "Relative Tuning Frequency"; + d.description = "Tuning frequency of channel 2, if channel 1 is assumed to contain the same music as it at a tuning frequency of A=440Hz."; + d.unit = "hz"; + d.hasFixedBinCount = true; + d.binCount = 1; + d.hasKnownExtents = false; + d.isQuantized = false; + d.sampleType = OutputDescriptor::VariableSampleRate; + d.hasDuration = false; + m_outputs[d.identifier] = list.size(); + list.push_back(d); + + d.identifier = "reffeature"; + d.name = "Reference Feature"; + d.description = "Chroma feature from reference audio."; + d.unit = ""; + d.hasFixedBinCount = true; + d.binCount = m_bpo; + d.hasKnownExtents = false; + d.isQuantized = false; + d.sampleType = OutputDescriptor::FixedSampleRate; + d.sampleRate = 1; + d.hasDuration = false; + m_outputs[d.identifier] = list.size(); + list.push_back(d); + + d.identifier = "otherfeature"; + d.name = "Other Feature"; + d.description = "Chroma feature from other audio, before rotation."; + d.unit = ""; + d.hasFixedBinCount = true; + d.binCount = m_bpo; + d.hasKnownExtents = false; + d.isQuantized = false; + d.sampleType = OutputDescriptor::FixedSampleRate; + d.sampleRate = 1; + d.hasDuration = false; + m_outputs[d.identifier] = list.size(); + list.push_back(d); + + d.identifier = "rotfeature"; + d.name = "Other Feature at Rotated Frequency"; + d.description = "Chroma feature from reference audio calculated with the tuning frequency obtained from rotation matching."; + d.unit = ""; + d.hasFixedBinCount = true; + d.binCount = m_bpo; + d.hasKnownExtents = false; + d.isQuantized = false; + d.sampleType = OutputDescriptor::FixedSampleRate; + d.sampleRate = 1; + d.hasDuration = false; + m_outputs[d.identifier] = list.size(); + list.push_back(d); + + return list; +} + +bool +TuningDifference::initialise(size_t channels, size_t stepSize, size_t blockSize) +{ + if (channels < getMinChannelCount() || + channels > getMaxChannelCount()) return false; + + if (stepSize != blockSize) return false; + + m_blockSize = blockSize; + + reset(); + + return true; +} + +void +TuningDifference::reset() +{ + if (m_frameCount > 0) { + m_refChroma.reset(new Chromagram(paramsForTuningFrequency(440.))); + m_frameCount = 0; + } + m_refTotals = TFeature(m_bpo, 0.0); + m_other.clear(); +} + +template<typename T> +void addTo(vector<T> &a, const vector<T> &b) +{ + transform(a.begin(), a.end(), b.begin(), a.begin(), plus<T>()); +} + +template<typename T> +T distance(const vector<T> &a, const vector<T> &b) +{ + return inner_product(a.begin(), a.end(), b.begin(), T(), + plus<T>(), [](T x, T y) { return fabs(x - y); }); +} + +TuningDifference::TFeature +TuningDifference::computeFeatureFromTotals(const TFeature &totals) const +{ + if (m_frameCount == 0) return totals; + + TFeature feature(m_bpo); + double sum = 0.0; + + for (int i = 0; i < m_bpo; ++i) { + double value = totals[i] / m_frameCount; + feature[i] += value; + sum += value; + } + + for (int i = 0; i < m_bpo; ++i) { + feature[i] /= sum; + } + + cerr << "computeFeatureFromTotals: feature values:" << endl; + for (auto v: feature) cerr << v << " "; + cerr << endl; + + return feature; +} + +Chromagram::Parameters +TuningDifference::paramsForTuningFrequency(double hz) const +{ + Chromagram::Parameters params(m_inputSampleRate); + params.lowestOctave = 0; + params.octaveCount = 6; + params.binsPerOctave = m_bpo; + params.tuningFrequency = hz; + params.atomHopFactor = 0.5; + return params; +} + +TuningDifference::TFeature +TuningDifference::computeFeatureFromSignal(const Signal &signal, double hz) const +{ + Chromagram chromagram(paramsForTuningFrequency(hz)); + + TFeature totals(m_bpo, 0.0); + + cerr << "computeFeatureFromSignal: hz = " << hz << ", frame count = " << m_frameCount << endl; + + for (int i = 0; i < m_frameCount; ++i) { + Signal::const_iterator first = signal.begin() + i * m_blockSize; + Signal::const_iterator last = first + m_blockSize; + if (last > signal.end()) last = signal.end(); + CQBase::RealSequence input(first, last); + input.resize(m_blockSize); + CQBase::RealBlock block = chromagram.process(input); + for (const auto &v: block) addTo(totals, v); + } + + return computeFeatureFromTotals(totals); +} + +TuningDifference::FeatureSet +TuningDifference::process(const float *const *inputBuffers, Vamp::RealTime) +{ + CQBase::RealBlock block; + CQBase::RealSequence input; + + input = CQBase::RealSequence + (inputBuffers[0], inputBuffers[0] + m_blockSize); + block = m_refChroma->process(input); + for (const auto &v: block) addTo(m_refTotals, v); + + m_other.insert(m_other.end(), + inputBuffers[1], inputBuffers[1] + m_blockSize); + + ++m_frameCount; + return FeatureSet(); +} + +double +TuningDifference::featureDistance(const TFeature &other, int rotation) const +{ + if (rotation == 0) { + return distance(m_refFeature, other); + } else { + // A positive rotation pushes the tuning frequency up for this + // chroma, negative one pulls it down. If a positive rotation + // makes this chroma match an un-rotated reference, then this + // chroma must have initially been lower than the reference. + TFeature r(other); + if (rotation < 0) { + rotate(r.begin(), r.begin() - rotation, r.end()); + } else { + rotate(r.begin(), r.end() - rotation, r.end()); + } + return distance(m_refFeature, r); + } +} + +int +TuningDifference::findBestRotation(const TFeature &other) const +{ + map<double, int> dists; + + int maxSemis = 6; + int maxRotation = (m_bpo * maxSemis) / 12; + + for (int r = -maxRotation; r <= maxRotation; ++r) { + double dist = featureDistance(other, r); + dists[dist] = r; + cerr << "rotation " << r << ": score " << dist << endl; + } + + int best = dists.begin()->second; + + cerr << "best is " << best << endl; + return best; +} + +pair<int, double> +TuningDifference::findFineFrequency(int coarseCents, double coarseScore) +{ + int coarseResolution = 1200 / m_bpo; + int searchDistance = coarseResolution/2 - 1; + + double bestScore = coarseScore; + int bestCents = coarseCents; + double bestHz = frequencyForCentsAbove440(coarseCents); + + cerr << "corresponding coarse Hz " << bestHz << " scores " << coarseScore << endl; + cerr << "searchDistance = " << searchDistance << endl; + + for (int sign = -1; sign <= 1; sign += 2) { + for (int offset = 1; offset <= searchDistance; ++offset) { + + int fineCents = coarseCents + sign * offset; + + cerr << "trying with fineCents = " << fineCents << "..." << endl; + + double fineHz = frequencyForCentsAbove440(fineCents); + TFeature fineFeature = computeFeatureFromSignal(m_other, fineHz); + double fineScore = featureDistance(fineFeature); + + cerr << "fine offset = " << offset << ", cents = " << fineCents + << ", Hz = " << fineHz << ", score " << fineScore + << " (best score so far " << bestScore << ")" << endl; + + if (fineScore < bestScore) { + cerr << "is good!" << endl; + bestScore = fineScore; + bestCents = fineCents; + bestHz = fineHz; + } else { + break; + } + } + } + + //!!! could keep a vector of scores & then interpolate... + + return pair<int, double>(bestCents, bestHz); +} + +TuningDifference::FeatureSet +TuningDifference::getRemainingFeatures() +{ + FeatureSet fs; + if (m_frameCount == 0) return fs; + + m_refFeature = computeFeatureFromTotals(m_refTotals); + TFeature otherFeature = computeFeatureFromSignal(m_other, 440.); + + Feature f; + + f.values.clear(); + for (auto v: m_refFeature) f.values.push_back(v); + fs[m_outputs["reffeature"]].push_back(f); + + f.values.clear(); + for (auto v: otherFeature) f.values.push_back(v); + fs[m_outputs["otherfeature"]].push_back(f); + + int rotation = findBestRotation(otherFeature); + + int coarseCents = -(rotation * 1200) / m_bpo; + + cerr << "rotation " << rotation << " -> cents " << coarseCents << endl; + + double coarseHz = frequencyForCentsAbove440(coarseCents); + + TFeature coarseFeature = computeFeatureFromSignal(m_other, coarseHz); + double coarseScore = featureDistance(coarseFeature); + + cerr << "corresponding Hz " << coarseHz << " scores " << coarseScore << endl; + + //!!! This should be returning the fine chroma, not the coarse + f.values.clear(); + for (auto v: coarseFeature) f.values.push_back(v); + fs[m_outputs["rotfeature"]].push_back(f); + + pair<int, double> fine = findFineFrequency(coarseCents, coarseScore); + int fineCents = fine.first; + double fineHz = fine.second; + + f.values.clear(); + f.values.push_back(fineHz); + fs[m_outputs["tuningfreq"]].push_back(f); + + f.values.clear(); + f.values.push_back(fineCents); + fs[m_outputs["cents"]].push_back(f); + + cerr << "overall best Hz = " << fineHz << endl; + + return fs; +} +