HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_metric_query.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <functional>
4#include <initializer_list>
5#include <utility> // pair
6#include <vector>
7
10#include "metrics/hart_metrics_common.hpp" // ReducerResultType
11#include "hart_reducers.hpp" // first
12#include "hart_slice.hpp"
13#include "hart_units.hpp" // Unit
14#include "hart_utils.hpp" // make_unique()
15
16namespace hart
17{
18
19/// @brief Manages the metrics calculations
20/// @details This object is meant to be created by metric functions, see @ref Metrics.
21/// Usage examples:
22/// @code
23/// const double resultA = samplePeak (monoBuffer); // Default unit, implicit cast to double
24/// const double resultB = samplePeak (monoBuffer).get(); // Default unit, explicit getter
25/// const double resultC = samplePeak (monoBuffer).as (dB); // Request to calculate in dB
26/// const double resultD = samplePeak (monoBuffer).as (linear); // Request to calculate in linear domain (as voltage, not dB)
27/// const double resultE = samplePeak (monoBuffer).as (native); // Request to calculate in metric's native unit
28/// const double resultF = samplePeak (monoBuffer).slice (100, 200).get(); // Peak, observed between 100th (inclusive) and 200th (non-inclusive) frames
29/// const double resultG = samplePeak (multiChannelBuffer).as (dB).get (max()); // Get peak of the loudest channel, in dB
30/// const double resultH = samplePeak (multiChannelBuffer).as (linear).get (nth (4)); // Get peak of the 4th channel (zero-based), as a linear value
31/// const double resultI = samplePeak (multiChannelBuffer).ch ({3, 0, 5}).get (max ()); // Peak, calculated only for channels 3, 0 and 5, max value (requested order of channels will be preserved for order-sensitive reducers)
32/// const size_t resultJ = samplePeak (multiChannelBuffer).get (argmax()); // Get the index of the loudest channel
33/// const vector<double> resultK = samplePeak (multiChannelBuffer).as (dB).get (collect()); // Get vector of per-channel peak values in dB
34/// @endcode
35///
36/// For a more detailed "how-to", see @ref UsingMetricsAndReducers.
37///
38/// If you create your own metric functions, it's strongly encouraged for them to return this class instance,
39/// rather that just a POD value, as this class handles multi-channel setups, slices etc.
40/// @ingroup Metrics
41template <typename ValueType>
43{
44public:
45 /// @brief A lambda function (or a callable object) that calculates a specific metric for a given channel
46 /// @details Arguments:
47 /// - `channel` - number of channel to calculate metric (for metrics that operate per channel), see `ch()`
48 /// - `sliceStart`, `sliceStop` - the range of data to calculate the metric on, see `slice()`
49 /// - `requestedUnit` - the unit that the metric should be calculated in, see `as()`
50 using SingleChannelMetricEvaluator = std::function<ValueType (size_t channel, Slice slice, Unit requestedUnit)>;
51
52 /// @brief A lambda function (or a callable object) that calculates a specific metric for a given pair of channels
53 /// @details Arguments:
54 /// - `channelA` - number of left-hand-side channel to calculate metric (for metrics that operate per channel), see `ch()`
55 /// - `channelB` - number of right-hand-side channel to calculate metric (for metrics that operate per channel), see `ch()`
56 /// - `sliceStart`, `sliceStop` - the range of data to calculate the metric on, see `slice()`
57 /// - `requestedUnit` - the unit that the metric should be calculated in, see `as()`
58 using ChannelPairMetricEvaluator = std::function<ValueType (size_t channelA, size_t channelB, Slice slice, Unit requestedUnit)>;
59
60 /// @brief Create a metric query object for a metric that operates on one channel at a time
61 /// @details This ctor in meant to be invoked by the metric functions,
62 /// as they return an object of this type.
63 /// @param evaluator A callable object that calculates a specific metric, created by the metric function
64 /// @param totalNumChannels Total number of channels in the received AudioBuffer or other container, observed by the metric function
65 /// @param totalLength Total length of the container (for one channel) passed to the metric function, observed by the metric function.
66 /// For instance, number of frames in an AudioBuffer, or number of bins in a Spectrum, per one channel.
67 /// @param defaultChannelsToProcess Subset of channels to process by default, if `ch()` wasn't called.
69 SingleChannelMetricEvaluator evaluator,
70 size_t totalNumChannels,
71 std::vector<size_t>&& defaultChannelsToProcess
72 )
73 {
74 m_query = hart::make_unique<Query>();
75 m_query->singleChannelMetricEvaluator = std::move (evaluator);
76 m_query->totalNumChannelsA = totalNumChannels;
77 m_query->totalNumChannelsB = 0;
78 m_query->slice = Slice::whole();
79 m_query->requestedUnit = Unit::native;
80 m_query->channels = std::move (defaultChannelsToProcess);
81 }
82
83 /// @brief Create a metric query object for a metric that operates on pair of channels at a time
84 /// @details This ctor in meant to be invoked by the metric functions,
85 /// as they return an object of this type.
86 /// @param evaluator A callable object that calculates a specific metric, created by the metric function
87 /// @param totalNumChannelsA Total number of channels in the received left-hand-side AudioBuffer or other container, observed by the metric function
88 /// @param totalNumChannelsB Total number of channels in the received right-hand-side AudioBuffer or other container, observed by the metric function.
89 /// If metric requires pairs of channels, but operates on a single container, it's expected to be equal to totalNumChannelsA.
90 /// @param totalLength Total length of the container (for one channel) passed to the metric function, observed by the metric function.
91 /// For instance, number of frames in an AudioBuffer, or number of bins in a Spectrum, per one channel.
92 /// Left-hand-side and right-hand-side containers are typically expected to be equal in size (per each channel).
93 /// @param defaultChannelPairsToProcess Subset of channel pairs to process by default, if `ch()` wasn't called.
95 ChannelPairMetricEvaluator evaluator,
96 size_t totalNumChannelsA,
97 size_t totalNumChannelsB,
98 std::vector<std::pair<size_t, size_t>>&& defaultChannelPairsToProcess
99 )
100 {
101 m_query = hart::make_unique<Query>();
102 m_query->channelPairMetricEvaluator = std::move (evaluator);
103 m_query->totalNumChannelsA = totalNumChannelsA;
104 m_query->totalNumChannelsB = totalNumChannelsB;
105 m_query->slice = Slice::whole();
106 m_query->requestedUnit = Unit::native;
107 m_query->channelPairs = std::move (defaultChannelPairsToProcess);
108 }
109
110 /// @brief Requests the metric to return its value(s) in a certain unit
111 /// @param requestedUnit A desired unit that the metric should return.
112 /// Refer to the documentation of a specific metric for supported units.
113 /// If unsupported unit is requested, the metric is expected to throw a
114 /// `hart::UnitError` exception.
115 MetricQuery as (Unit requestedUnit) const
116 {
117 MetricQuery copy (*this);
118 copy.m_query = hart::make_unique<Query> (*m_query);
119 copy.m_query->requestedUnit = requestedUnit;
120 return copy;
121 }
122
123 /// @brief Requests the metric to be applied to certain channel.
124 /// @details If this method is not called, the channel subset will be
125 /// defined by a specific metric.
126 /// @param channel Zero-based channel index to measure. You may
127 /// use values from @ref hart::Channel or @ref hart::MidSideChannel
128 /// where appropriate.
129 MetricQuery ch (size_t channel) const
130 {
131 MetricQuery copy (*this);
132 copy.m_query = hart::make_unique<Query> (*m_query);
133
134 if (getEvaluatorType() == EvaluatorType::singleChannels)
135 {
136 copy.m_query->channels.clear();
137 copy.m_query->channels.push_back (channel);
138 }
139 else
140 {
141 copy.m_query->channelPairs.clear();
142 copy.m_query->channelPairs.push_back (std::make_pair (channel, channel));
143 }
144
145 return copy;
146 }
147
148 /// @brief Requests the metric to be applied to certain channels.
149 /// @details If this method is not called, the channel subset will be
150 /// defined by a specific metric.
151 /// @param channels List of zero-based channel indices to measure. You may
152 /// use values from @ref hart::Channel or @ref hart::MidSideChannel where
153 /// appropriate. The order of values handed to the reducer matches the order
154 /// of channel indices in `channels`.
155 MetricQuery ch (std::initializer_list<size_t> channels) const
156 {
157 MetricQuery copy (*this);
158 copy.m_query = hart::make_unique<Query> (*m_query);
159
160 if (getEvaluatorType() == EvaluatorType::singleChannels)
161 {
162 copy.m_query->channels.clear();
163 copy.m_query->channels.reserve (channels.size());
164 copy.m_query->channels.assign (channels.begin(), channels.end());
165 }
166 else
167 {
168 copy.m_query->channelPairs.clear();
169 copy.m_query->channelPairs.reserve (channels.size());
170
171 for (const size_t channel : channels)
172 copy.m_query->channelPairs.emplace_back (channel, channel);
173 }
174
175 return copy;
176 }
177
178 /// @brief Requests the metric to be applied to certain channel pairs.
179 /// @details If this method is not called, the channel pair subset will
180 /// be defined by a specific metric.
181 /// @param channels List of pairs of zero-based channel indices to measure.
182 /// You may use values from @ref hart::Channel or @ref hart::MidSideChannel
183 /// wherever appropriate. The order of values handed to the reducer matches
184 /// the order of channel indices in `channels`.
185 MetricQuery ch (std::initializer_list<std::pair<size_t, size_t>> channels) const
186 {
187 MetricQuery copy (*this);
188 copy.m_query = hart::make_unique<Query> (*m_query);
189
190 if (getEvaluatorType() == EvaluatorType::singleChannels)
191 HART_THROW_OR_RETURN (hart::UnsupportedError, "The metric does not support channel pairs", copy);
192
193 copy.m_query->channelPairs.clear();
194 copy.m_query->channels.reserve (channels.size());
195 copy.m_query->channelPairs.assign (channels.begin(), channels.end());
196 return copy;
197 }
198
199 /// @brief Requests the metric to be applied to certain channels.
200 /// @details This method is meant to be called by the matchers, as the selected
201 /// channels are stored in a ChannelFrags object.
202 /// @param channelFlags set of per-channel flags. Channels marked as `true`
203 /// will be included, ones marked as `false` will be skipped.
204 MetricQuery ch (const ChannelFlags& channelFlags) const
205 {
206 MetricQuery copy (*this);
207 copy.m_query = hart::make_unique<Query> (*m_query);
208
209 if (getEvaluatorType() == EvaluatorType::singleChannels)
210 {
211 copy.m_query->channels.clear();
212 copy.m_query->channels.reserve (channelFlags.numTrue());
213
214 for (size_t channel = 0; channel < channelFlags.size(); ++ channel)
215 if (channelFlags[channel] == true)
216 copy.m_query->channels.push_back (channel);
217 }
218 else
219 {
220 copy.m_query->channelPairs.clear();
221 copy.m_query->channelPairs.reserve (channelFlags.numTrue());
222
223 for (size_t channel = 0; channel < channelFlags.size(); ++ channel)
224 if (channelFlags[channel] == true)
225 copy.m_query->channelPairs.emplace_back (channel, channel);
226 }
227
228 return copy;
229 }
230
231 /// @brief Requests to perform a metric on a specific range inside of data
232 /// @param slice Slice representing a range of data, see `hart::Slice`
233 /// @throws hart::SizeError If the the slice is empty
234 MetricQuery at (Slice slice) const
235 {
236 MetricQuery copy (*this);
237 copy.m_query = hart::make_unique<Query> (*m_query);
238
239 if (slice.isEmpty())
240 HART_THROW_OR_RETURN (hart::SizeError, "Requested slice is empty", copy);
241
242 copy.m_query->slice = slice;
243 return copy;
244 }
245
246 /// @brief Query a value of a calculated metric using a reducer
247 /// @details Typically, metrics are calculated per channel, which result in a
248 /// vector of per-channel values. And in most cases, you just want one scalar,
249 /// like a max, min, mean etc. You can choose how to reduce a vector to one
250 /// scalar, by providing a reducer. There's a good chance one the built-in
251 /// reducers will do what you're looking for, see @ref Reducers. Reducer can
252 /// also be a lambda or a callable object. Reducers usually return just one
253 /// scalar value, but not always - for example, hart::collect() will just
254 /// forward the per-channel vector of values.
255 /// @param reducer Callable reducer that accepts two iterators over per-channel
256 /// metric values
257 template <typename ReducerType>
258 auto get (ReducerType reducer) const
259 -> ReducerResultType<ReducerType, typename std::vector<ValueType>::const_iterator>
260 {
261 ensureCache();
262 return reducer (m_query->cachedValues.begin(), m_query->cachedValues.end());
263 }
264
265 /// @brief Query a value of a calculated metric
266 /// @details This overload is useful for mono signals, or metrics that calculate
267 /// a scalar value, rather than calculating a per-channel vector of values. For
268 /// metrics that are calculated per channel, use an overload of this method that
269 /// takes a reducer callable.
270 ValueType get() const
271 {
272 return get (first());
273 }
274
275 /// @brief Query a value of a calculated metric
276 /// @details This cast is useful for mono signals, or metrics that calculate
277 /// a scalar value, rather than calculating a per-channel vector of values. For
278 /// metrics that are calculated per channel, use an overload of this method that
279 /// takes a reducer callable.
280 operator ValueType() const
281 {
282 return get (first());
283 }
284
285private:
286 struct Query
287 {
288 SingleChannelMetricEvaluator singleChannelMetricEvaluator = nullptr;
289 ChannelPairMetricEvaluator channelPairMetricEvaluator = nullptr;
290 std::vector<size_t> channels;
291 std::vector<std::pair<size_t, size_t>> channelPairs;
292 size_t totalNumChannelsA = 0;
293 size_t totalNumChannelsB = 0;
294 Slice slice {Slice::whole()};
295 Unit requestedUnit = Unit::native;
296 mutable bool cacheValid = false;
297 mutable std::vector<ValueType> cachedValues;
298 };
299
300 enum class EvaluatorType
301 {
302 singleChannels,
303 channelPairs,
304 none
305 };
306
307 EvaluatorType getEvaluatorType() const
308 {
309 hassert (m_query != nullptr);
310
311 if (m_query->singleChannelMetricEvaluator != nullptr)
312 {
313 hassert (m_query->channelPairMetricEvaluator == nullptr);
314 return EvaluatorType::singleChannels;
315 }
316
317 if (m_query->channelPairMetricEvaluator != nullptr)
318 {
319 hassert (m_query->singleChannelMetricEvaluator == nullptr);
320 return EvaluatorType::channelPairs;
321 }
322
323 hassertfalse; // Unexpected state - both evaluators are empty!
324 return EvaluatorType::none;
325 }
326
327 void ensureCache() const
328 {
329 if (m_query->cacheValid)
330 return;
331
332 m_query->cachedValues.clear();
333
334 if (getEvaluatorType() == EvaluatorType::singleChannels)
335 buildCacheForSingleChannelsEvaluator();
336 else
337 buildCacheForChannelPairsEvaluator();
338
339 m_query->cacheValid = true;
340 }
341
342 void buildCacheForSingleChannelsEvaluator() const
343 {
344 for (size_t channel : m_query->channels)
345 {
346 if (channel >= m_query->totalNumChannelsA)
347 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Requested channel index is out of range");
348
349 m_query->cachedValues.push_back (
350 m_query->singleChannelMetricEvaluator (
351 channel,
352 m_query->slice,
353 m_query->requestedUnit
354 )
355 );
356 }
357 }
358
359 void buildCacheForChannelPairsEvaluator() const
360 {
361 for (const std::pair<size_t, size_t>& channelPair : m_query->channelPairs)
362 {
363 if (channelPair.first >= m_query->totalNumChannelsA || channelPair.second >= m_query->totalNumChannelsB)
364 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Requested channel index is out of range");
365
366 m_query->cachedValues.push_back (
367 m_query->channelPairMetricEvaluator (
368 channelPair.first,
369 channelPair.second,
370 m_query->slice,
371 m_query->requestedUnit
372 )
373 );
374 }
375 }
376
377 std::shared_ptr<Query> m_query;
378};
379
380} // namespace hart
A set of boolean flags mapped to each audio channel.
bool operator[](size_t channel) const
Access the flag value for a specific channel.
size_t size() const noexcept
Returns the size (not capacity) of the container.
size_t numTrue() const noexcept
Checks how many channels are marked with true
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.
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.
MetricQuery ch(std::initializer_list< std::pair< size_t, size_t > > channels) const
Requests the metric to be applied to certain channel pairs.
MetricQuery ch(std::initializer_list< size_t > channels) const
Requests the metric to be applied to certain channels.
MetricQuery ch(size_t channel) const
Requests the metric to be applied to certain channel.
operator ValueType() const
Query a value of a calculated metric.
MetricQuery ch(const ChannelFlags &channelFlags) const
Requests the metric to be applied to certain channels.
MetricQuery at(Slice slice) const
Requests to perform a metric on a specific range inside of data.
ValueType get() const
Query a value of a calculated metric.
auto get(ReducerType reducer) const -> ReducerResultType< ReducerType, typename std::vector< ValueType >::const_iterator >
Query a value of a calculated metric using a reducer.
MetricQuery as(Unit requestedUnit) const
Requests the metric to return its value(s) in a certain unit.
Thrown when an unexpected container size is encountered.
Thrown when some parameter has an unsupported value.
#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.
#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 ...
#define hassertfalse
Triggers a HartAssertException
Unit
Represents a physical unit.
@ native
Default (native) unit of whatever returns some value.
Represents a slice of analysis data.
static Slice whole()
bool isEmpty() const
Returns the first element in the range.