Chris@47: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@47: Chris@47: /* Chris@47: Centre for Digital Music, Queen Mary University of London. Chris@47: Chris@47: This program is free software; you can redistribute it and/or Chris@47: modify it under the terms of the GNU General Public License as Chris@47: published by the Free Software Foundation; either version 2 of the Chris@47: License, or (at your option) any later version. See the file Chris@47: COPYING included with this distribution for more information. Chris@47: */ Chris@47: Chris@55: #include "TuningDifference.h" Chris@47: Chris@47: #include Chris@47: Chris@47: #include Chris@47: #include Chris@47: #include Chris@47: Chris@47: #include Chris@47: #include Chris@47: Chris@47: using namespace std; Chris@47: Chris@47: static double pitchToFrequency(int pitch, Chris@47: double centsOffset = 0., Chris@47: double concertA = 440.) Chris@47: { Chris@47: double p = double(pitch) + (centsOffset / 100.); Chris@47: return concertA * pow(2.0, (p - 69.0) / 12.0); Chris@47: } Chris@47: Chris@47: static double frequencyForCentsAbove440(double cents) Chris@47: { Chris@47: return pitchToFrequency(69, cents, 440.); Chris@47: } Chris@47: Chris@47: static float defaultMaxDuration = 0.f; Chris@50: static int defaultMaxSemis = 5; Chris@47: static bool defaultFineTuning = true; Chris@47: Chris@55: TuningDifference::TuningDifference(float inputSampleRate) : Chris@47: Plugin(inputSampleRate), Chris@47: m_channelCount(0), Chris@47: m_bpo(120), Chris@47: m_blockSize(0), Chris@47: m_frameCount(0), Chris@47: m_maxDuration(defaultMaxDuration), Chris@47: m_maxSemis(defaultMaxSemis), Chris@47: m_fineTuning(defaultFineTuning) Chris@47: { Chris@47: } Chris@47: Chris@55: TuningDifference::~TuningDifference() Chris@47: { Chris@47: } Chris@47: Chris@47: string Chris@55: TuningDifference::getIdentifier() const Chris@47: { Chris@55: return "tuning-difference"; Chris@47: } Chris@47: Chris@47: string Chris@55: TuningDifference::getName() const Chris@47: { Chris@55: return "Tuning Difference"; Chris@47: } Chris@47: Chris@47: string Chris@55: TuningDifference::getDescription() const Chris@47: { Chris@47: return "Estimate the tuning frequencies of a set of recordings at once, by comparing them to a reference recording of the same music whose tuning frequency is known"; Chris@47: } Chris@47: Chris@47: string Chris@55: TuningDifference::getMaker() const Chris@47: { Chris@47: return "Chris Cannam"; Chris@47: } Chris@47: Chris@47: int Chris@55: TuningDifference::getPluginVersion() const Chris@47: { Chris@47: // Increment this each time you release a version that behaves Chris@47: // differently from the previous one Chris@47: return 3; Chris@47: } Chris@47: Chris@47: string Chris@55: TuningDifference::getCopyright() const Chris@47: { Chris@47: // This function is not ideally named. It does not necessarily Chris@47: // need to say who made the plugin -- getMaker does that -- but it Chris@47: // should indicate the terms under which it is distributed. For Chris@47: // example, "Copyright (year). All Rights Reserved", or "GPL" Chris@47: return "GPL"; Chris@47: } Chris@47: Chris@55: TuningDifference::InputDomain Chris@55: TuningDifference::getInputDomain() const Chris@47: { Chris@47: return TimeDomain; Chris@47: } Chris@47: Chris@47: size_t Chris@55: TuningDifference::getPreferredBlockSize() const Chris@47: { Chris@47: return 0; Chris@47: } Chris@47: Chris@47: size_t Chris@55: TuningDifference::getPreferredStepSize() const Chris@47: { Chris@47: return 0; Chris@47: } Chris@47: Chris@47: size_t Chris@55: TuningDifference::getMinChannelCount() const Chris@47: { Chris@47: return 2; Chris@47: } Chris@47: Chris@47: size_t Chris@55: TuningDifference::getMaxChannelCount() const Chris@47: { Chris@52: return 1000; Chris@47: } Chris@47: Chris@55: TuningDifference::ParameterList Chris@55: TuningDifference::getParameterDescriptors() const Chris@47: { Chris@47: ParameterList list; Chris@47: Chris@47: ParameterDescriptor desc; Chris@47: Chris@47: desc.identifier = "maxduration"; Chris@47: desc.name = "Maximum duration to analyse"; Chris@47: desc.description = "The maximum duration (in seconds) to consider from either input file, always taken from the start of the input. Zero means there is no limit."; Chris@47: desc.minValue = 0; Chris@47: desc.maxValue = 3600; Chris@47: desc.defaultValue = defaultMaxDuration; Chris@47: desc.isQuantized = false; Chris@47: desc.unit = "s"; Chris@47: list.push_back(desc); Chris@47: Chris@47: desc.identifier = "maxrange"; Chris@47: desc.name = "Maximum range in semitones"; Chris@47: desc.description = "The maximum difference in semitones that will be searched."; Chris@47: desc.minValue = 1; Chris@47: desc.maxValue = 11; Chris@47: desc.defaultValue = defaultMaxSemis; Chris@47: desc.isQuantized = true; Chris@47: desc.quantizeStep = 1; Chris@47: desc.unit = "semitones"; Chris@47: list.push_back(desc); Chris@47: Chris@47: desc.identifier = "finetuning"; Chris@47: desc.name = "Fine tuning"; Chris@47: desc.description = "Use a fine tuning stage to increase nominal resolution from 10 cents to 1 cent."; Chris@47: desc.minValue = 0; Chris@47: desc.maxValue = 1; Chris@47: desc.defaultValue = (defaultFineTuning ? 1.f : 0.f); Chris@47: desc.isQuantized = true; Chris@47: desc.quantizeStep = 1; Chris@47: desc.unit = ""; Chris@47: list.push_back(desc); Chris@47: Chris@47: return list; Chris@47: } Chris@47: Chris@47: float Chris@55: TuningDifference::getParameter(string id) const Chris@47: { Chris@47: if (id == "maxduration") { Chris@47: return m_maxDuration; Chris@47: } else if (id == "maxrange") { Chris@47: return float(m_maxSemis); Chris@47: } else if (id == "finetuning") { Chris@47: return m_fineTuning ? 1.f : 0.f; Chris@47: } Chris@47: return 0; Chris@47: } Chris@47: Chris@47: void Chris@55: TuningDifference::setParameter(string id, float value) Chris@47: { Chris@47: if (id == "maxduration") { Chris@47: m_maxDuration = value; Chris@47: } else if (id == "maxrange") { Chris@47: m_maxSemis = int(roundf(value)); Chris@47: } else if (id == "finetuning") { Chris@47: m_fineTuning = (value > 0.5f); Chris@47: } Chris@47: } Chris@47: Chris@55: TuningDifference::ProgramList Chris@55: TuningDifference::getPrograms() const Chris@47: { Chris@47: ProgramList list; Chris@47: return list; Chris@47: } Chris@47: Chris@47: string Chris@55: TuningDifference::getCurrentProgram() const Chris@47: { Chris@47: return ""; // no programs Chris@47: } Chris@47: Chris@47: void Chris@55: TuningDifference::selectProgram(string) Chris@47: { Chris@47: } Chris@47: Chris@55: TuningDifference::OutputList Chris@55: TuningDifference::getOutputDescriptors() const Chris@47: { Chris@47: OutputList list; Chris@47: Chris@47: OutputDescriptor d; Chris@47: d.identifier = "cents"; Chris@47: d.name = "Tuning Differences"; Chris@47: d.description = "A single feature vector containing a value for each input channel after the first (reference) channel, containing the difference in averaged frequency profile between that channel and the reference channel, in cents. A positive value means the corresponding channel is higher than the reference."; Chris@47: d.unit = "cents"; Chris@54: d.hasFixedBinCount = true; Chris@47: if (m_channelCount > 1) { Chris@47: d.binCount = m_channelCount - 1; Chris@47: } else { Chris@54: d.binCount = 1; Chris@47: } Chris@47: d.hasKnownExtents = false; Chris@47: d.isQuantized = false; Chris@47: d.sampleType = OutputDescriptor::VariableSampleRate; Chris@47: d.hasDuration = false; Chris@47: m_outputs[d.identifier] = int(list.size()); Chris@47: list.push_back(d); Chris@47: Chris@47: d.identifier = "tuningfreq"; Chris@47: d.name = "Relative Tuning Frequencies"; Chris@47: d.description = "A single feature vector containing a value for each input channel after the first (reference) channel, containing the tuning frequency of that channel, if the reference channel is assumed to contain the same music as it at a tuning frequency of A=440Hz."; Chris@47: d.unit = "hz"; Chris@54: d.hasFixedBinCount = true; Chris@47: if (m_channelCount > 1) { Chris@47: d.binCount = m_channelCount - 1; Chris@47: } else { Chris@54: d.binCount = 1; Chris@47: } Chris@47: d.hasKnownExtents = false; Chris@47: d.isQuantized = false; Chris@47: d.sampleType = OutputDescriptor::VariableSampleRate; Chris@47: d.hasDuration = false; Chris@47: m_outputs[d.identifier] = int(list.size()); Chris@47: list.push_back(d); Chris@47: Chris@47: d.identifier = "reffeature"; Chris@47: d.name = "Reference Feature"; Chris@47: d.description = "Chroma feature from reference channel."; Chris@47: d.unit = ""; Chris@47: d.hasFixedBinCount = true; Chris@47: d.binCount = m_bpo; Chris@47: d.hasKnownExtents = false; Chris@47: d.isQuantized = false; Chris@47: d.sampleType = OutputDescriptor::FixedSampleRate; Chris@47: d.sampleRate = 1; Chris@47: d.hasDuration = false; Chris@47: m_outputs[d.identifier] = int(list.size()); Chris@47: list.push_back(d); Chris@47: Chris@47: d.identifier = "otherfeature"; Chris@47: d.name = "Other Features"; Chris@47: d.description = "Series of chroma feature vectors from the non-reference audio channels, before rotation."; Chris@47: d.unit = ""; Chris@47: d.hasFixedBinCount = true; Chris@47: d.binCount = m_bpo; Chris@47: d.hasKnownExtents = false; Chris@47: d.isQuantized = false; Chris@47: d.sampleType = OutputDescriptor::FixedSampleRate; Chris@47: d.sampleRate = 1; Chris@47: d.hasDuration = false; Chris@47: m_outputs[d.identifier] = int(list.size()); Chris@47: list.push_back(d); Chris@47: Chris@47: d.identifier = "rotfeature"; Chris@47: d.name = "Other Features at Rotated Frequency"; Chris@48: d.description = "Series of chroma feature vectors from the non-reference audio channels, calculated with the tuning frequency obtained from rotation matching. Note that this does not take into account any fine tuning, only the basic rotation match."; Chris@47: d.unit = ""; Chris@47: d.hasFixedBinCount = true; Chris@47: d.binCount = m_bpo; Chris@47: d.hasKnownExtents = false; Chris@47: d.isQuantized = false; Chris@47: d.sampleType = OutputDescriptor::FixedSampleRate; Chris@47: d.sampleRate = 1; Chris@47: d.hasDuration = false; Chris@47: m_outputs[d.identifier] = int(list.size()); Chris@47: list.push_back(d); Chris@47: Chris@47: return list; Chris@47: } Chris@47: Chris@47: bool Chris@55: TuningDifference::initialise(size_t channels, size_t stepSize, size_t blockSize) Chris@47: { Chris@47: if (channels < getMinChannelCount()) return false; Chris@47: if (stepSize != blockSize) return false; Chris@47: if (m_blockSize > INT_MAX) return false; Chris@47: Chris@50: m_channelCount = int(channels); Chris@47: m_blockSize = int(blockSize); Chris@47: Chris@47: reset(); Chris@47: Chris@47: return true; Chris@47: } Chris@47: Chris@47: void Chris@55: TuningDifference::reset() Chris@47: { Chris@50: Chromagram::Parameters params(paramsForTuningFrequency(440.)); Chris@50: m_reference.clear(); Chris@50: m_refChroma.reset(new Chromagram(params)); Chris@50: m_refTotals = TFeature(m_bpo, 0.0); Chris@50: m_refFeatures.clear(); Chris@50: m_otherChroma.clear(); Chris@50: for (int i = 1; i < m_channelCount; ++i) { Chris@50: m_otherChroma.push_back(std::make_shared(params)); Chris@47: } Chris@50: m_otherTotals = vector(m_channelCount-1, TFeature(m_bpo, 0.0)); Chris@50: m_frameCount = 0; Chris@47: } Chris@47: Chris@47: template Chris@47: void addTo(vector &a, const vector &b) Chris@47: { Chris@69: int n = int(b.size()); Chris@69: Chris@69: for (int i = 0; i < n; ++i) { Chris@69: int j = (i == 0 ? n-1 : i-1); Chris@69: T diff = b[i] - b[j]; Chris@69: a[i] += diff; Chris@69: } Chris@47: } Chris@47: Chris@47: template Chris@47: T distance(const vector &a, const vector &b) Chris@47: { Chris@47: return inner_product(a.begin(), a.end(), b.begin(), T(), Chris@47: plus(), [](T x, T y) { return fabs(x - y); }); Chris@47: } Chris@47: Chris@55: TuningDifference::TFeature Chris@55: TuningDifference::computeFeatureFromTotals(const TFeature &totals) const Chris@47: { Chris@47: if (m_frameCount == 0) return totals; Chris@47: Chris@47: TFeature feature(m_bpo); Chris@69: double max = 0.0; Chris@47: Chris@47: for (int i = 0; i < m_bpo; ++i) { Chris@47: double value = totals[i] / m_frameCount; Chris@69: feature[i] = value; Chris@69: if (fabs(value) > max) { Chris@69: max = fabs(value); Chris@69: } Chris@47: } Chris@47: Chris@69: if (max > 0.0) { Chris@50: for (int i = 0; i < m_bpo; ++i) { Chris@69: feature[i] /= max; Chris@50: } Chris@47: } Chris@47: Chris@47: return feature; Chris@47: } Chris@47: Chris@47: Chromagram::Parameters Chris@55: TuningDifference::paramsForTuningFrequency(double hz) const Chris@47: { Chris@47: Chromagram::Parameters params(m_inputSampleRate); Chris@47: params.lowestOctave = 2; Chris@47: params.octaveCount = 4; Chris@47: params.binsPerOctave = m_bpo; Chris@47: params.tuningFrequency = hz; Chris@47: params.atomHopFactor = 0.5; Chris@47: params.window = CQParameters::Hann; Chris@47: return params; Chris@47: } Chris@47: Chris@55: TuningDifference::TFeature Chris@55: TuningDifference::computeFeatureFromSignal(const Signal &signal, Chris@55: double hz) const Chris@47: { Chris@47: Chromagram chromagram(paramsForTuningFrequency(hz)); Chris@47: Chris@47: TFeature totals(m_bpo, 0.0); Chris@47: Chris@47: cerr << "computeFeatureFromSignal: hz = " << hz << ", frame count = " << m_frameCount << endl; Chris@47: Chris@47: for (int i = 0; i < m_frameCount; ++i) { Chris@47: Signal::const_iterator first = signal.begin() + i * m_blockSize; Chris@47: Signal::const_iterator last = first + m_blockSize; Chris@47: if (last > signal.end()) last = signal.end(); Chris@47: CQBase::RealSequence input(first, last); Chris@47: input.resize(m_blockSize); Chris@47: CQBase::RealBlock block = chromagram.process(input); Chris@47: for (const auto &v: block) addTo(totals, v); Chris@47: } Chris@47: Chris@47: return computeFeatureFromTotals(totals); Chris@47: } Chris@47: Chris@55: TuningDifference::FeatureSet Chris@55: TuningDifference::process(const float *const *inputBuffers, Vamp::RealTime) Chris@47: { Chris@47: if (m_maxDuration > 0) { Chris@47: int maxFrames = int((m_maxDuration * m_inputSampleRate) / Chris@47: float(m_blockSize)); Chris@47: if (m_frameCount > maxFrames) return FeatureSet(); Chris@47: } Chris@50: Chris@47: CQBase::RealBlock block; Chris@47: CQBase::RealSequence input; Chris@47: Chris@47: input = CQBase::RealSequence Chris@47: (inputBuffers[0], inputBuffers[0] + m_blockSize); Chris@47: block = m_refChroma->process(input); Chris@47: for (const auto &v: block) addTo(m_refTotals, v); Chris@47: Chris@50: if (m_fineTuning) { Chris@50: m_reference.insert(m_reference.end(), Chris@50: inputBuffers[0], Chris@50: inputBuffers[0] + m_blockSize); Chris@50: } Chris@50: Chris@47: for (int c = 1; c < m_channelCount; ++c) { Chris@50: input = CQBase::RealSequence Chris@50: (inputBuffers[c], inputBuffers[c] + m_blockSize); Chris@50: block = m_otherChroma[c-1]->process(input); Chris@50: for (const auto &v: block) addTo(m_otherTotals[c-1], v); Chris@47: } Chris@47: Chris@47: ++m_frameCount; Chris@47: return FeatureSet(); Chris@47: } Chris@47: Chris@55: TuningDifference::FeatureSet Chris@55: TuningDifference::getRemainingFeatures() Chris@47: { Chris@47: FeatureSet fs; Chris@47: if (m_frameCount == 0) return fs; Chris@47: Chris@50: m_refFeatures[0] = computeFeatureFromTotals(m_refTotals); Chris@47: Chris@47: Feature f; Chris@47: f.hasTimestamp = true; Chris@47: f.timestamp = Vamp::RealTime::zeroTime; Chris@47: f.values.clear(); Chris@47: fs[m_outputs["cents"]].push_back(f); Chris@47: fs[m_outputs["tuningfreq"]].push_back(f); Chris@47: Chris@47: for (int c = 1; c < m_channelCount; ++c) { Chris@47: getRemainingFeaturesForChannel(c, fs); Chris@47: } Chris@47: Chris@47: return fs; Chris@47: } Chris@47: Chris@47: void Chris@55: TuningDifference::getRemainingFeaturesForChannel(int channel, Chris@55: FeatureSet &fs) Chris@47: { Chris@50: TFeature otherFeature = Chris@50: computeFeatureFromTotals(m_otherTotals[channel-1]); Chris@47: Chris@47: Feature f; Chris@47: f.hasTimestamp = true; Chris@47: f.timestamp = Vamp::RealTime::zeroTime; Chris@47: Chris@47: f.values.clear(); Chris@50: for (auto v: m_refFeatures[0]) f.values.push_back(float(v)); Chris@47: fs[m_outputs["reffeature"]].push_back(f); Chris@47: Chris@47: f.values.clear(); Chris@47: for (auto v: otherFeature) f.values.push_back(float(v)); Chris@47: fs[m_outputs["otherfeature"]].push_back(f); Chris@47: Chris@50: int rotation = findBestRotation(m_refFeatures[0], otherFeature); Chris@47: Chris@47: int coarseCents = -(rotation * 1200) / m_bpo; Chris@47: Chris@47: cerr << "channel " << channel << ": rotation " << rotation << " -> cents " << coarseCents << endl; Chris@47: Chris@50: TFeature rotatedFeature = otherFeature; Chris@47: if (rotation != 0) { Chris@50: rotateFeature(rotatedFeature, rotation); Chris@47: } Chris@47: Chris@47: f.values.clear(); Chris@50: for (auto v: rotatedFeature) f.values.push_back(float(v)); Chris@47: fs[m_outputs["rotfeature"]].push_back(f); Chris@47: Chris@48: if (m_fineTuning) { Chris@48: Chris@55: pair fine = Chris@55: findFineFrequency(rotatedFeature, coarseCents); Chris@50: Chris@48: int fineCents = fine.first; Chris@48: double fineHz = fine.second; Chris@47: Chris@48: fs[m_outputs["cents"]][0].values.push_back(float(fineCents)); Chris@48: fs[m_outputs["tuningfreq"]][0].values.push_back(float(fineHz)); Chris@47: Chris@48: cerr << "channel " << channel << ": overall best Hz = " << fineHz << endl; Chris@48: Chris@48: } else { Chris@48: Chris@48: fs[m_outputs["cents"]][0].values.push_back(float(coarseCents)); Chris@48: fs[m_outputs["tuningfreq"]][0].values.push_back Chris@48: (float(frequencyForCentsAbove440(coarseCents))); Chris@48: } Chris@47: } Chris@47: Chris@50: void Chris@55: TuningDifference::rotateFeature(TFeature &r, int rotation) const Chris@50: { Chris@50: if (rotation < 0) { Chris@50: rotate(r.begin(), r.begin() - rotation, r.end()); Chris@50: } else { Chris@50: rotate(r.begin(), r.end() - rotation, r.end()); Chris@50: } Chris@50: } Chris@50: Chris@50: double Chris@55: TuningDifference::featureDistance(const TFeature &ref, Chris@55: const TFeature &other, Chris@55: int rotation) const Chris@50: { Chris@50: if (rotation == 0) { Chris@50: return distance(ref, other); Chris@50: } else { Chris@50: // A positive rotation pushes the tuning frequency up for this Chris@50: // chroma, negative one pulls it down. If a positive rotation Chris@50: // makes this chroma match an un-rotated reference, then this Chris@50: // chroma must have initially been lower than the reference. Chris@50: TFeature r(other); Chris@50: rotateFeature(r, rotation); Chris@50: return distance(ref, r); Chris@50: } Chris@50: } Chris@50: Chris@50: int Chris@55: TuningDifference::findBestRotation(const TFeature &ref, Chris@55: const TFeature &other) const Chris@50: { Chris@50: map dists; Chris@50: Chris@50: int maxRotation = (m_bpo * m_maxSemis) / 12; Chris@50: Chris@50: for (int r = -maxRotation; r <= maxRotation; ++r) { Chris@50: double dist = featureDistance(ref, other, r); Chris@50: dists[dist] = r; Chris@50: } Chris@50: Chris@50: int best = dists.begin()->second; Chris@50: Chris@50: return best; Chris@50: } Chris@50: Chris@50: pair Chris@55: TuningDifference::findFineFrequency(const TFeature &rotatedOtherFeature, Chris@55: int coarseCents) Chris@50: { Chris@50: int coarseResolution = 1200 / m_bpo; Chris@50: int searchDistance = coarseResolution/2 - 1; Chris@50: Chris@50: int bestCents = coarseCents; Chris@50: double bestHz = frequencyForCentsAbove440(coarseCents); Chris@50: Chris@50: cerr << "findFineFrequency: coarse frequency is " << bestHz << endl; Chris@50: cerr << "searchDistance = " << searchDistance << endl; Chris@50: Chris@50: double bestScore = 0; Chris@50: bool firstScore = true; Chris@50: Chris@50: for (int sign = -1; sign <= 1; sign += 2) { Chris@50: for (int offset = (sign < 0 ? 0 : 1); Chris@50: offset <= searchDistance; Chris@50: ++offset) { Chris@50: Chris@50: int fineCents = coarseCents + sign * offset; Chris@50: double fineHz = frequencyForCentsAbove440(fineCents); Chris@50: Chris@50: cerr << "trying with fineCents = " << fineCents << "..." << endl; Chris@50: Chris@50: // compare the rotated "other" chroma with a reference Chris@50: // chroma shifted by the offset in the opposite direction Chris@50: Chris@50: int compensatingCents = -sign * offset; Chris@50: TFeature compensatedReference; Chris@50: Chris@50: if (m_refFeatures.find(compensatingCents) == m_refFeatures.end()) { Chris@50: double compensatingHz = frequencyForCentsAbove440 Chris@50: (compensatingCents); Chris@50: Chris@50: compensatedReference = computeFeatureFromSignal Chris@50: (m_reference, compensatingHz); Chris@50: Chris@50: m_refFeatures[compensatingCents] = compensatedReference; Chris@50: Chris@50: } else { Chris@50: Chris@50: compensatedReference = m_refFeatures[compensatingCents]; Chris@50: } Chris@50: Chris@50: double fineScore = featureDistance(compensatedReference, Chris@50: rotatedOtherFeature, Chris@50: 0); // we are rotated already Chris@50: Chris@50: cerr << "fine offset = " << offset << ", cents = " << fineCents Chris@50: << ", Hz = " << fineHz << ", score " << fineScore Chris@50: << " (best score so far " << bestScore << ")" << endl; Chris@50: Chris@50: if ((fineScore < bestScore) || firstScore) { Chris@50: cerr << "is good!" << endl; Chris@50: bestScore = fineScore; Chris@50: bestCents = fineCents; Chris@50: bestHz = fineHz; Chris@50: firstScore = false; Chris@50: } else { Chris@50: break; Chris@50: } Chris@50: } Chris@50: } Chris@50: Chris@50: return pair(bestCents, bestHz); Chris@50: } Chris@50: