HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_channel_correlation.hpp
Go to the documentation of this file.
1#pragma once
2
6#include "metrics/hart_metric_query.hpp"
7#include "metrics/hart_metrics_common.hpp" // ChannelSubsets
8#include "hart_slice.hpp"
9#include "hart_units.hpp" // Unit
10#include "hart_utils.hpp" // nan()
11
12namespace hart
13{
14
15/// @brief Calculates zero-lag normalized cross-correlation between two channels of an audio buffer
16/// @details Operates per specified pairs of channels, use a reducer to get a scalar value
17/// (see @ref Reducers). If custom channel subset is not specified via `ch()`, defaults to a set of
18/// all unique unordered channel pairs, e.g. `{{0, 1}}` for stereo buffer, or
19/// `{{0, 1}, {0, 2}, {1, 2}, {1, 3}, {2, 3}}` for 3-channel buffer. A specific order of pairs is
20/// not guaranteed, unless you explicitly pass a custom list of channel pairs via `ch()`.
21///
22/// Returns a unitless value, suppoert `Unit::none` and `Unit::native`, which are the same here,
23/// so there's no need to request any unit with a chained `as()` call.
24///
25/// Usage examples
26/// @code
27/// // All defaults - correlation between channels 0 and 1 (left and right)
28/// channelCorrelation (stereoBuffer).get();
29///
30/// // Highest value of three cross-correlations (channels 0 vs 1, 0 vs 2 and 1 vs 3)
31/// channelCorrelation (multichannelBuffer).ch ({{0, 1}, {0, 2}, {1, 3}}).get (max());
32/// @endcode
33///
34/// Be careful when you want to specify only one channel pair:
35/// @code
36/// // /!\ Resolves to 2 matched channel pairs - 0 vs 0 and 1 vs 1.
37/// // Probably not what you're looking for!
38/// channelCorrelation (stereoBuffer).ch ({0, 1});
39///
40/// // Just one pair - 0 vs 1. Note the double curly braces.
41/// channelCorrelation (stereoBuffer).ch ({{0, 1}});
42/// @endcode
43///
44/// Uses the normalized cross-correlation formula:
45/// @f[
46/// \rho = \frac{\sum_n x[n]\,y[n]}
47/// {\sqrt{\left(\sum_n x[n]^2\right)\left(\sum_n y[n]^2\right)}}
48/// @f]
49///
50/// (`sum (x[n] * y[n]) / sqrt (sum (x[n]^2) * sum (y[n]^2))`)
51///
52/// where `x` and `y` are the selected channels of the same buffer.
53///
54/// The returned value is in the range `[-1, 1]`:
55/// - `1.0` means perfectly correlated channels
56/// - `0.0` means no linear correlation
57/// - `-1.0` means perfectly inverted polarity
58///
59/// The function returns `NaN` if correlation is undefined, such as when:
60/// - one of the selected channels is silent
61/// - the buffer contains zero frames
62///
63/// @param buffer Input audio buffer
64/// @returns Chainable `MetricQuery`, which calculates normalized correlation coefficient
65/// per pair of channels, or `NaN` if correlation is undefined
66/// @tparam SampleType Floating point sample type, typically `float` or `double`
67/// @throws hart::IndexError if either channel index is out of bounds
68/// @ingroup Metrics
69template <typename SampleType>
70MetricQuery<double> channelCorrelation (const AudioBuffer<SampleType>& buffer)
71{
72 typename MetricQuery<double>::ChannelPairMetricEvaluator evaluator =
73 [&buffer]
74 (size_t channelA, size_t channelB, Slice slice, Unit requestedUnit)
75 -> double
76 {
77 const double nan = hart::nan<double>();
78
79 if (channelA >= buffer.getNumChannels())
80 HART_THROW_OR_RETURN (hart::IndexError, "Channel A index is out of bounds", nan);
81
82 if (channelB >= buffer.getNumChannels())
83 HART_THROW_OR_RETURN (hart::IndexError, "Channel B index is out of bounds", nan);
84
85 if (requestedUnit != Unit::native && requestedUnit != Unit::none)
86 HART_THROW_OR_RETURN (hart::UnitError, "Channel correlation cannot be calculated in a requested unit", nan);
87
88 if (slice.isEmpty())
89 return hart::nan<double>();
90
91 const auto sliceFrameIndices = buffer.getFrameIndices (slice);
92 const size_t sliceStart = sliceFrameIndices.first;
93 const size_t sliceStop = sliceFrameIndices.second;
94 hassert (sliceStart < sliceStop);
95 hassert (sliceStop - sliceStart != 0);
96 hassert (sliceStop <= buffer.getNumFrames());
97
98 // If channel A and B point to the same channel, we still want to go through the whole thing,
99 // as it can be either 1.0 or NaN depending on the contents
100
101 const SampleType* channelAData = buffer[channelA];
102 const SampleType* channelBData = buffer[channelB];
103
104 AccurateSum<double> dotProduct { 0.0 };
105 AccurateSum<double> sumSqChannelA { 0.0 };
106 AccurateSum<double> sumSqChannelB { 0.0 };
107
108 for (size_t frame = sliceStart; frame < sliceStop; ++frame)
109 {
110 const double channelAValue = static_cast<double> (channelAData[frame]);
111 const double channelBValue = static_cast<double> (channelBData[frame]);
112
113 dotProduct += channelAValue * channelBValue;
114 sumSqChannelA += channelAValue * channelAValue;
115 sumSqChannelB += channelBValue * channelBValue;
116 }
117
118 if (floatsEqual<double> (sumSqChannelA, 0.0) || floatsEqual<double> (sumSqChannelB, 0.0))
119 return nan;
120
121 return dotProduct / std::sqrt (sumSqChannelA * sumSqChannelB);
122 };
123
124 const size_t numChannels = buffer.getNumChannels();
125 return MetricQuery<double> (
126 std::move (evaluator),
127 numChannels,
128 numChannels,
130 );
131}
132
133} // namespace hart
Implements Kahan algorithm for floating point accumulations.
AccurateSum(SampleType initialSum=(SampleType) 0)
Inits AccurateSum with a specific value.
AccurateSum & operator+=(SampleType value)
Adds a value to a sum, tracking the potential floating point error.
Container for audio data.
Thrown when a container index is out of range.
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 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 > channelCorrelation(const AudioBuffer< SampleType > &buffer)
Calculates zero-lag normalized cross-correlation between two channels of an audio buffer.
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
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.
@ none
Unitless value.
@ 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 > > upperTriangleChannelPairs(size_t numChannels)
Represents a slice of analysis data.
bool isEmpty() const