HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_log_spectral_distance.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // max()
4#include <cmath> // isnan(), sqrt(), log()
5#include <complex> // norm()
6#include <utility> // pair
7
10#include "metrics/hart_metric_query.hpp"
11#include "metrics/hart_metrics_common.hpp" // ChannelSubsets
12#include "hart_slice.hpp"
14#include "hart_units.hpp" // Unit
15#include "hart_utils.hpp" // nan(), floatsEqual(), centsToRatio()
16
17// TODO: Document it properly
18
19namespace hart
20{
21
22/// @brief Calculates difference between two spectra in log-frequency domain
23/// @ingroup Metrics
24inline MetricQuery<double> logSpectralDistance (const Spectrum& spectrumA, const Spectrum& spectrumB, Normalise normaliseLevels = Normalise::no, double smoothingCents = 100.0)
25{
26 if (smoothingCents < 0.0 || floatsEqual (smoothingCents, 0.0))
27 HART_THROW_OR_RETURN (ValueError, "smoothingCents should be a non-negative band width", {});
28
30 HART_THROW_OR_RETURN (SampleRateError, "Sample rates of the two spectra must match", {});
31
32 if (spectrumA.getFFTSize() != spectrumB.getFFTSize())
33 HART_THROW_OR_RETURN (SizeError, "FFT sizes of the two spectra must match", {});
34
35 hassert (spectrumA.getNumBins() == spectrumB.getNumBins()); // If FFT sizes match, so should the numbers of bins
36
37 typename MetricQuery<double>::ChannelPairMetricEvaluator evaluator =
38 [&spectrumA, &spectrumB, normaliseLevels, smoothingCents]
39 (size_t spectrumAChannel, size_t spectrumBChannel, Slice slice, Unit requestedUnit)
40 -> double
41 {
42 if (requestedUnit != Unit::native && requestedUnit != Unit::dB)
43 HART_THROW_OR_RETURN (hart::UnitError, "Unsupported unit", hart::nan<double>());
44
45 hassert (spectrumAChannel < spectrumA.getNumChannels());
46 hassert (spectrumBChannel < spectrumB.getNumChannels());
47 hassert (! std::isnan (spectrumA.getSampleRateHz()));
48 hassert (! std::isnan (spectrumB.getSampleRateHz()));
49
50 const std::pair<size_t, size_t> binIndices = spectrumA.getBinIndices (slice);
51 const size_t startBin = std::max<size_t> (1, binIndices.first);
52 const size_t stopBin = binIndices.second;
53
54 if (slice.isEmpty() || stopBin - startBin == 0)
55 return hart::nan<double>();
56
57 hassert (startBin < stopBin);
58 hassert (stopBin <= spectrumA.getNumBins());
59
60 // TODO: Reserve space in those two-vectors
61 std::vector<double> levelsADb;
62 std::vector<double> levelsBDb;
63 AccurateSum<double> levelDifferenceSum;
64 const double bandRatio = centsToRatio (smoothingCents);
65 const double stopFrequencyHz = spectrumA.getBinFrequencyHz (stopBin - 1);
66 double currentBandStartHz = spectrumA.getBinFrequencyHz (startBin);
67 double currentBandEndHz = currentBandStartHz * bandRatio;
68
69 while (currentBandEndHz <= stopFrequencyHz)
70 {
71 AccurateSum<double> bandPowerA;
72 AccurateSum<double> bandPowerB;
73 size_t numCurrentBandBins = 0;
74
75 for (size_t bin = startBin; bin < stopBin; ++bin)
76 {
77 const double frequencyHz = spectrumA.getBinFrequencyHz (bin);
78
79 if (frequencyHz < currentBandStartHz)
80 continue;
81
82 if (frequencyHz >= currentBandEndHz)
83 break;
84
85 bandPowerA += std::norm (spectrumA.getBinValue (spectrumAChannel, bin));
86 bandPowerB += std::norm (spectrumB.getBinValue (spectrumBChannel, bin));
87 ++numCurrentBandBins;
88 }
89
90 if (numCurrentBandBins > 0)
91 {
92 const double binMeanPowerA = bandPowerA.getValue() / static_cast<double> (numCurrentBandBins);
93 const double binMeanPowerB = bandPowerB.getValue() / static_cast<double> (numCurrentBandBins);
94
95 const double levelADb = powerToDecibels (binMeanPowerA);
96 const double levelBDb = powerToDecibels (binMeanPowerB);
97
98 levelsADb.push_back (levelADb);
99 levelsBDb.push_back (levelBDb);
100
101 // For optional gain normalization
102 levelDifferenceSum += (levelBDb - levelADb);
103 }
104
105 currentBandStartHz = currentBandEndHz;
106 currentBandEndHz = currentBandStartHz * bandRatio;
107 }
108
109 hassert (levelsADb.size() == levelsBDb.size());
110 const size_t numPoints = levelsADb.size();
111
112 if (numPoints == 0)
113 return hart::nan<double>(); // At least one of the signals is silent
114
115 const double offsetDb =
116 normaliseLevels == Normalise::no
117 ? 0.0
118 : levelDifferenceSum.getValue() / static_cast<double> (numPoints);
119
120 AccurateSum<double> squaredErrorSum;
121
122 for (size_t i = 0; i < numPoints; ++i)
123 {
124 const double errorDb = levelsADb[i] - levelsBDb[i] + offsetDb;
125 squaredErrorSum += errorDb * errorDb;
126 }
127
128 return std::sqrt (squaredErrorSum.getValue() / static_cast<double> (numPoints));
129 };
130
131 const size_t numPairs = std::min (spectrumA.getNumChannels(), spectrumB.getNumChannels());
132 return MetricQuery<double> (
133 std::move (evaluator),
134 spectrumA.getNumChannels(),
135 spectrumB.getNumChannels(),
137 );
138}
139
140} // namespace hart
Implements Kahan algorithm for floating point accumulations.
SampleType getValue() const
AccurateSum & operator+=(SampleType value)
Adds a value to a sum, tracking the potential floating point error.
Manages the metrics calculations.
MetricQuery(ChannelPairMetricEvaluator evaluator, size_t totalNumChannelsA, size_t totalNumChannelsB, std::vector< std::pair< size_t, size_t > > &&defaultChannelPairsToProcess)
Create a metric query object for a metric that operates on pair of channels at a time.
Thrown when sample rate is mismatched or invalid.
Thrown when an unexpected container size is encountered.
Frequency-domain representation of a multi-channel audio signal.
size_t getFFTSize() const
Returns FFT size.
double getSampleRateHz() const
Returns sample rate in Hz.
std::pair< size_t, size_t > getBinIndices(const Slice &slice) const
Returns a pair of indices representing a provided slice.
double getBinFrequencyHz(size_t binIndex) const
Returns frequency corresponding to a bin index.
size_t getNumBins() const
Returns number of frequency bins per channel.
std::complex< double > getBinValue(size_t channel, size_t binIndex) const
Returns complex value of a frequency bin, by bin index.
size_t getNumChannels() const
Returns number of channels.
Thrown when some metric is requested to return a value in an unsupported unit.
Thrown when an inappropriate value is encountered.
#define hassert(condition)
Triggers a HartAssertException if the condition is false
#define HART_THROW_OR_RETURN(ExceptionType, message, returnValue)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns a specified ...
MetricQuery< double > logSpectralDistance(const Spectrum &spectrumA, const Spectrum &spectrumB, Normalise normaliseLevels=Normalise::no, double smoothingCents=100.0)
Calculates difference between two spectra in log-frequency domain.
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
Normalise
Helper values for something that could normalise something.
static SampleType floatsNotEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
static SampleType powerToDecibels(SampleType valueLinear)
Converts linear value (power) to dB.
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
double centsToRatio(double cents)
Converts frequency difference in cents to frequence ratio.
Unit
Represents a physical unit.
@ dB
Value of something in decibels. Can represent voltage, power, or a domain-specific unit like "LUFS" o...
@ native
Default (native) unit of whatever returns some value.
Helpers to generate common default channel subsets.
static std::vector< std::pair< size_t, size_t > > diagonalChannelPairs(size_t numChannels)
Represents a slice of analysis data.
bool isEmpty() const