HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_correlation_latency_detector.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // max()
4#include <sstream>
5#include <vector>
6
12#include "hart_utils.hpp" // make_unique(), roundToSizeT(), inf, floatsEqual()
13
14namespace hart
15{
16
17/// @brief Correlation-based latency detector implementation for the hart::LatencyBelow class. For internal use.
18/// @private
19template <typename SampleType>
20class CorrelationLatencyDetector :
21 public LatencyDetector<SampleType>
22{
23public:
24 CorrelationLatencyDetector (double maxLatencySeconds, SilencePolicy silencePolicy, double absCorrelationThreshold) :
25 m_maxLatencySeconds (maxLatencySeconds),
26 m_silencePolicy (silencePolicy),
27 m_absCorrelationThreshold (absCorrelationThreshold)
28 {
29 if (absCorrelationThreshold > 1.0)
30 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Normalized correlation threshold can not be higher than 1");
31
32 if (absCorrelationThreshold < 0.0)
33 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Correlation threshold is an absolute value, so should not be negative");
34
35 // Technically, zero correlation is okay, but a bit too weird...
36 if (floatsEqual (absCorrelationThreshold, 0.0))
37 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Zero correlation threshold is not a meaningful value in latency detector context");
38 }
39
40 std::unique_ptr<LatencyDetector<SampleType>> copy() const override
41 {
42 return hart::make_unique<CorrelationLatencyDetector<SampleType>> (*this);
43 }
44
45 void prepare (double sampleRateHz, size_t /* numChannels */, size_t /* maxBlockSizeFrames */) override
46 {
47 m_sampleRateHz = sampleRateHz;
48 }
49
50 void reset() override
51 {
52 m_hadValidData = false;
53 m_failureChannel = 0;
54 m_failureFrame = 0;
55 m_detectedLatencyFrames = 0;
56 m_bestCorrelation = 0.0;
57 }
58
59 bool match (
60 const AudioBuffer<SampleType>& inputAudio,
61 const AudioBuffer<SampleType>& observedOutputAudio,
62 const std::function<bool (size_t)>& appliesToChannel
63 ) override
64 {
65 const size_t numFrames = inputAudio.getNumFrames();
66
67 if (numFrames == 0)
68 {
69 m_hadValidData = false;
70 return false;
71 }
72
73 // Very small overlaps can produce spuriously high normalized correlations near the
74 // signal boundary, so ignore lags that leave only a tiny slice of the buffer aligned.
75 // This keeps the detector from "preferring" an almost-full-duration lag that happens
76 // to correlate well on just a handful of samples.
77 const size_t onePercentOfDurationFrames = (numFrames + 99) / 100;
78 const size_t minOverlapFrames = std::max<size_t> (2, onePercentOfDurationFrames);
79
80 if (numFrames < minOverlapFrames)
81 {
82 m_hadValidData = false;
83 return false;
84 }
85
86 const size_t maxLagFrames = numFrames - minOverlapFrames;
87 bool anyValidChannel = false;
88 size_t worstLatencyFrames = 0;
89 size_t worstChannel = 0;
90
91 for (size_t channel = 0; channel < inputAudio.getNumChannels(); ++channel)
92 {
93 if (! appliesToChannel (channel))
94 continue;
95
96 const SampleType* x = inputAudio[channel];
97 const SampleType* y = observedOutputAudio[channel];
98 std::vector<double> prefixSumsSqX (numFrames + 1, 0.0);
99 std::vector<double> prefixSumsSqY (numFrames + 1, 0.0);
100 AccurateSum<double> runningSumSqX { 0.0 };
101 AccurateSum<double> runningSumSqY { 0.0 };
102
103 for (size_t frame = 0; frame < numFrames; ++frame)
104 {
105 const double xVal = static_cast<double> (x[frame]);
106 const double yVal = static_cast<double> (y[frame]);
107
108 runningSumSqX += xVal * xVal;
109 runningSumSqY += yVal * yVal;
110 prefixSumsSqX[frame + 1] = runningSumSqX;
111 prefixSumsSqY[frame + 1] = runningSumSqY;
112 }
113
114 double bestAbsCorrelation = -hart::inf;
115 size_t bestLag = 0;
116 bool channelValid = false;
117
118 for (size_t lag = 0; lag <= maxLagFrames; ++lag)
119 {
120 AccurateSum<double> dotProduct = { 0.0 };
121 const size_t inputOverlapBeginFrame = 0;
122 const size_t outputOverlapBeginFrame = lag;
123 const size_t overlapSizeFrames = numFrames - lag;
124
125 if (overlapSizeFrames < minOverlapFrames)
126 break;
127
128 const size_t inputOverlapEndFrame = inputOverlapBeginFrame + overlapSizeFrames;
129 const size_t outputOverlapEndFrame = outputOverlapBeginFrame + overlapSizeFrames;
130 const double sumSqX = prefixSumsSqX[inputOverlapEndFrame] - prefixSumsSqX[inputOverlapBeginFrame];
131 const double sumSqY = prefixSumsSqY[outputOverlapEndFrame] - prefixSumsSqY[outputOverlapBeginFrame];
132
133 for (size_t overlapFrame = 0; overlapFrame < overlapSizeFrames; ++overlapFrame)
134 {
135 const double inputValue = static_cast<double> (x[inputOverlapBeginFrame + overlapFrame]);
136 const double outputValue = static_cast<double> (y[outputOverlapBeginFrame + overlapFrame]);
137 dotProduct += inputValue * outputValue;
138 }
139
140 if (floatsEqual (sumSqX, 0.0) || floatsEqual (sumSqY, 0.0))
141 continue;
142
143 channelValid = true;
144 const double correlation = dotProduct / std::sqrt (sumSqX * sumSqY);
145 const double absCorrelation = std::abs (correlation);
146
147 if (absCorrelation > bestAbsCorrelation)
148 {
149 bestAbsCorrelation = absCorrelation;
150 bestLag = lag;
151 }
152
153 if (floatsEqual (bestAbsCorrelation, 1.0))
154 break;
155 }
156
157 if (! channelValid || bestAbsCorrelation < m_absCorrelationThreshold)
158 {
159 if (m_silencePolicy == SilencePolicy::strict)
160 {
161 m_hadValidData = false;
162 m_failureChannel = channel;
163 return false;
164 }
165
166 continue;
167 }
168
169 anyValidChannel = true;
170
171 if (bestLag > worstLatencyFrames)
172 {
173 worstLatencyFrames = bestLag;
174 worstChannel = channel;
175 m_bestCorrelation = bestAbsCorrelation;
176 }
177 }
178
179 if (!anyValidChannel)
180 {
181 m_hadValidData = false;
182 return false;
183 }
184
185 m_hadValidData = true;
186 m_detectedLatencyFrames = worstLatencyFrames;
187 const double latencySeconds = m_detectedLatencyFrames / m_sampleRateHz;
188
189 if (latencySeconds <= m_maxLatencySeconds)
190 return true;
191
192 m_failureChannel = worstChannel;
193 return false;
194 }
195
196 MatcherFailureDetails getFailureDetails() const override
197 {
198 MatcherFailureDetails details;
199 details.channel = m_failureChannel;
200 details.frame = m_failureFrame;
201
202 if (! m_hadValidData)
203 {
204 details.description = "Latency could not be determined with sufficient correlation";
205 return details;
206 }
207
208 const double latencySeconds = m_detectedLatencyFrames / m_sampleRateHz;
209
210 std::stringstream descriptionStream;
211 descriptionStream
212 << "Detected latency: "
213 << secPrecision << latencySeconds << " seconds ("
214 << m_detectedLatencyFrames << " frames), "
215 << "best correlation: " << correlationPrecision << m_bestCorrelation;
216
217 details.description = descriptionStream.str();
218 return details;
219 }
220
221private:
222 const double m_maxLatencySeconds;
223 const SilencePolicy m_silencePolicy;
224 const double m_absCorrelationThreshold;
225 double m_sampleRateHz = 0.0;
226
227 bool m_hadValidData = false;
228 size_t m_detectedLatencyFrames = 0;
229 double m_bestCorrelation = 0.0;
230 size_t m_failureChannel = 0;
231 size_t m_failureFrame = 0;
232};
233
234} // 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.
Thrown when an inappropriate value is encountered.
#define HART_THROW_OR_RETURN_VOID(ExceptionType, message)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns otherwise.
std::ostream & secPrecision(std::ostream &stream)
Sets number of decimal places for values in seconds.
static std::ostream & correlationPrecision(std::ostream &stream)
Sets number of decimal places for correlation values.
SilencePolicy
Defines how silence in various algorithms.
constexpr double inf
Infinity.
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
size_t channel
Index of channel at which the failure was detected.
std::string description
Readable description of why the match has failed.
size_t frame
Index of frame at which the match has failed.