HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_snr.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <cmath> // min()
4
8#include "metrics/hart_metric_query.hpp"
9#include "metrics/hart_metrics_common.hpp" // ChannelSubsets
10#include "hart_slice.hpp"
11#include "hart_utils.hpp" // nan(), floatsEqual(), floatsNotEqual(), powerToDecibels()
12#include "hart_units.hpp" // Unit
13
14namespace hart
15{
16
17/// @brief Calculates signal-to-noise ratio (SNR)
18/// @details SNR expresses the ratio between the energy of a reference signal
19/// and the energy of the error, or noise, present in an estimated signal.
20///
21/// The noise component is calculated as the sample-by-sample difference
22/// between the estimated and reference signals.
23///
24/// SNR is calculated this way:
25/// @f[
26/// \mathrm{SNR}
27/// =
28/// \frac
29/// {\sum_{n=0}^{N-1} r[n]^2}
30/// {\sum_{n=0}^{N-1} \left(x[n] - r[n]\right)^2}
31/// @f]
32///
33/// (SNR = sum(r[n] ** 2) / sum((x[n] - r[n]) ** 2)),
34///
35/// where x[n] is a sample from the estimated signal, r[n] is the
36/// corresponding sample from the reference signal, and N is the number of
37/// frames being analyzed.
38///
39/// Higher values indicate a closer match to the reference signal, and thus
40/// lower noise. Identical signals produce positive infinity, and this metric
41/// will return `+inf` in those cases.
42///
43/// Can be expressed as an energy ratio or decibels. Supports `Unit::ratio`,
44/// `Unit::native` (same as ratio), and `Unit::dB`. Values in decibels are
45/// calculated as a power ratio:
46///
47/// @f[
48/// \mathrm{SNR_{dB}} = 10 \log_{10}\left(\mathrm{SNR}\right)
49/// @f]
50///
51/// (SNR_dB = 10 * log10(SNR)).
52///
53/// The two buffers are expected to represent aligned versions of the same
54/// signal. Differences in gain, latency, phase, or other deterministic signal
55/// properties are included in the measured noise/error.
56///
57/// @tparam SampleType
58/// @param signalPlusNoise Estimated or measured signal
59/// @param signal Reference signal to compare against
60/// @return Chainable `MetricQuery` object which calculates SNR as a linear
61/// energy ratio or in decibels. May return `NaN` or `+inf`.
62/// @ingroup Metrics
63template <typename SampleType>
64MetricQuery<double> snr (const AudioBuffer<SampleType>& signalPlusNoise, const AudioBuffer<SampleType>& signal)
65{
66 if (! signalPlusNoise.hasSampleRate() || signalPlusNoise.getSampleRateHz() < 0.0 || floatsEqual (signalPlusNoise.getSampleRateHz(), 0.0))
67 HART_THROW_OR_RETURN (SampleRateError, "signalPlusNoise must have a valid sample rate", nan<double>());
68
69 if (! signal.hasSampleRate() || signal.getSampleRateHz() < 0.0 || floatsEqual (signal.getSampleRateHz(), 0.0))
70 HART_THROW_OR_RETURN (SampleRateError, "signal must have a valid sample rate", nan<double>());
71
72 if (floatsNotEqual (signalPlusNoise.getSampleRateHz(), signal.getSampleRateHz()))
73 HART_THROW_OR_RETURN (SampleRateError, "Both provided buffers should have same saple rate", nan<double>());
74
75 MetricQuery<double>::SingleChannelMetricEvaluator evaluator =
76 [&signalPlusNoise, &signal]
77 (size_t channel, Slice slice, Unit requestedUnit)
78 -> double
79 {
80 const double sampleRateHz = signal.getSampleRateHz();
81
82 if (channel >= signalPlusNoise.getNumChannels())
83 HART_THROW_OR_RETURN (hart::IndexError, "Channel index is out of bounds for the signalPlusNoise buffer", nan<double>());
84
85 if (channel >= signal.getNumChannels())
86 HART_THROW_OR_RETURN (hart::IndexError, "Channel index is out of bounds for the signal buffer", nan<double>());
87
88 if (slice.isEmpty())
89 return nan<double>();
90
91 const auto sliceFrameIndices = signal.getFrameIndices (slice);
92 const size_t sliceStart = sliceFrameIndices.first;
93 const size_t sliceStop = sliceFrameIndices.second;
94 hassert (sliceStop > sliceStart);
95 hassert (sliceStop <= signal.getNumFrames());
96
97 const size_t numFrames = sliceStop - sliceStart;
98 hassert (numFrames != 0);
99
100 AccurateSum<double> signalEnergy;
101 AccurateSum<double> noiseEnergy;
102
103 const SampleType* signalChannelData = signal[channel] + sliceStart;
104 const SampleType* signalPlusNoiseChannelData = signalPlusNoise[channel] + sliceStart;
105
106 for (size_t frame = 0; frame < numFrames; ++frame)
107 {
108 const double s = static_cast<double> (signalChannelData[frame]);
109 const double spn = static_cast<double> (signalPlusNoiseChannelData[frame]);
110 const double n = spn - s;
111
112 signalEnergy += s * s;
113 noiseEnergy += n * n;
114 }
115
116 if (floatsEqual<double> (signalEnergy, 0.0))
117 return nan<double>();
118
119 // Congrats - no noise at all!
120 if (floatsEqual<double> (noiseEnergy, 0.0))
121 return inf; // Both ratio and dB are inf here
122
123 const double snrRatio = signalEnergy.getValue() / noiseEnergy.getValue();
124
125 switch (requestedUnit)
126 {
127 case Unit::native:
128 case Unit::ratio: return snrRatio;
129
130 case Unit::dB: return hart::powerToDecibels (snrRatio);
131
132 default: HART_THROW_OR_RETURN (hart::UnitError, "Unsupported unit", nan<double>());
133 }
134 };
135
136 const size_t numChannels = std::min (signal.getNumChannels(), signalPlusNoise.getNumChannels());
137 return MetricQuery<double> (
138 std::move (evaluator),
139 numChannels,
141 );
142}
143
144} // 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.
Thrown when a container index is out of range.
Manages the metrics calculations.
MetricQuery(SingleChannelMetricEvaluator evaluator, size_t totalNumChannels, std::vector< size_t > &&defaultChannelsToProcess)
Create a metric query object for a metric that operates on one channel at a time.
Thrown when sample rate is mismatched or invalid.
Thrown when some metric is requested to return a value in an unsupported unit.
#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 > snr(const AudioBuffer< SampleType > &signalPlusNoise, const AudioBuffer< SampleType > &signal)
Calculates signal-to-noise ratio (SNR)
Definition hart_snr.hpp:64
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
constexpr double inf
Infinity.
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.
Unit
Represents a physical unit.
@ dB
Value of something in decibels. Can represent voltage, power, or a domain-specific unit like "LUFS" o...
@ ratio
Generic ratio.
@ native
Default (native) unit of whatever returns some value.
Helpers to generate common default channel subsets.
static std::vector< size_t > allChannels(size_t numChannels)
Represents a slice of analysis data.
bool isEmpty() const