HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_process_audio.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // min()
4#include <cassert>
5#include <cmath>
6#include <functional>
7#include <iomanip>
8#include <memory>
9#include <sstream>
10#include <vector>
11
15#include "dsp/hart_dsp_all.hpp"
16#include "dsp/hart_dsp_function.hpp"
18#include "matchers/hart_matcher.hpp"
19#include "matchers/hart_matcher_function.hpp"
20#include "hart_plot.hpp"
24#include "signals/hart_signals_all.hpp"
25#include "hart_utils.hpp" // make_unique(), roundToSizeT()
26
27namespace hart {
28
29/// @defgroup TestRunner Test Runner
30/// @brief Runs the tests
31
32/// @brief Determines when to save a file
33/// @ingroup TestRunner
34enum class Save
35{
36 always, ///< File will be saved always, after the test is performed
37 whenFails, ///< File will be saved only when the test has failed
38 never ///< File will not be saved
39};
40
41/// @brief Determines whether to reset the Signal in a given context
42/// @ingroup TestRunner
43enum class ResetSignal
44{
45 no, ///< The signal will continue from whatever state it was in
46 yes ///< The signal's state will be reset
47};
48
49/// @brief A DSP host used for building and running tests inside a test case
50/// @ingroup TestRunner
51template <typename SampleType>
53{
54public:
55 /// @brief Moves the DSP instance into the host
56 /// @details DSP instance will be moved into this host, and then returned by @ref process(), so you can re-use it.
57 /// You can only pass a DSP by moving it, since some of the custom DSP wrappers can be non-copyable.
58 /// If you do want to copy a DSP instance here, use its DSPBase::copy() method explicitly.
59 /// @param dsp Your DSP instance
60 template <typename DSPType>
61 AudioTestBuilder (DSPType&& dsp,
62 typename std::enable_if<
63 ! std::is_lvalue_reference<DSPType&&>::value &&
64 std::is_base_of<DSPBase<SampleType>, typename std::decay<DSPType>::type>::value
65 >::type* = 0)
66 : m_processor (std::forward<DSPType> (dsp).move())
67 {
68 }
69
70 /// @brief Transfers the DSP smart pointer into the host
71 /// @details Use this if your DSP does not support copying or moving. It will be owned by this host,
72 /// and then returned by @ref process(), so you can re-use it.
73 /// @param dsp A smart pointer to your DSP instance
74 AudioTestBuilder (std::unique_ptr<DSPBase<SampleType>> dsp)
75 : m_processor (std::move (dsp))
76 {
77 }
78
79 /// @brief Sets the sample rate for the test
80 /// @details All the signals, effects and sub hosts are guaranteed to be initialized to this sample rate
81 /// @param sampleRateHz Sample rate in Hz. You can use frequency-related literails from @ref Units.
82 AudioTestBuilder& withSampleRate (double sampleRateHz)
83 {
84 if (sampleRateHz <= 0)
85 HART_THROW_OR_RETURN (hart::ValueError, "Sample rate should be a positive value in Hz", *this);
86
87 if (! m_processor->supportsSampleRate (sampleRateHz))
88 HART_THROW_OR_RETURN (hart::SampleRateError, "Sample rate is not supported by the tested DSP", *this);
89
90 m_sampleRateHz = sampleRateHz;
91 return *this;
92 }
93
94 /// @brief Sets the block size for the test
95 /// @param blockSizeFrames Block size in frames (samples)
96 AudioTestBuilder& withBlockSize (size_t blockSizeFrames)
97 {
98 if (blockSizeFrames == 0)
99 HART_THROW_OR_RETURN (hart::SizeError, "Illegal block size - should be a positive value in frames (samples)", *this);
100
101 m_blockSizeFrames = blockSizeFrames;
102 return *this;
103 }
104
105 /// @brief Sets the initial param value for the tested DSP
106 /// @details It will call @ref DSP::setValue() for DSP under test
107 /// @param id Parameter ID (see @ref DSP::setValue())
108 /// @param value Value that needs to be set
109 AudioTestBuilder& withValue (int id, double value)
110 {
111 // TODO: Handle cases when processor already has an envelope for this id
112 paramValues.emplace_back (ParamValue { id, value });
113 return *this;
114 }
115
116 /// @brief Sets the total duration of the input signal to be processed
117 /// @param durationSeconds of the signal in seconds. You can use time-related literails from @ref Units.
118 AudioTestBuilder& withDuration (double durationSeconds)
119 {
120 if (durationSeconds < 0)
121 HART_THROW_OR_RETURN(hart::ValueError, "Signal duration should be a non-negative value in seconds", *this);
122
123 m_testDurationSeconds = durationSeconds;
124 return *this;
125 }
126
127 /// @brief Sets whether to call reset() and/or prepare() on DSP testee before rendering audio
128 /// @details It is useful when you re-use your DSP instance, to control whether you want to
129 /// preserve its pre-existing state. If you also request `withWarmUp()`, this call will only
130 /// affect pre-warp-up preparation. Post-warm-up preparaton is selected via `withWarmUp()` args.
132 {
133 m_dspPreparationBeforeWarmUp = dspPreparation;
134 return *this;
135 }
136
137 /// @brief Sets whether to call reset() and/or prepare() on the input Signal before rendering audio
138 /// @details It is useful when you re-use your Signal instance, to control whether you want to
139 /// preserve its pre-existing state. If you also request `withWarmUp()`, this call will only
140 /// affect pre-warp-up preparation. Post-warm-up preparaton is selected via `withWarmUp()` args.
142 {
143 m_signalPreparationBeforeWarmUp = signalPreparation;
144 return *this;
145 }
146
147 /// @brief Adds a warm‑up period before the main test.
148 /// @details The signal will be processed for this time, but no matchers will be invoked.
149 /// This can be useful if your DSP uses parameter smoothers internally, that need to settle
150 /// before performing the test, or has some sort of attack envelope stage, like a compressor,
151 /// that you want to skip. This time will be added up with a regular test render run, i.e.
152 /// `processAudioWith (...).withDuration (100_ms).withWarmUp (10_ms)` will result in
153 /// 10 + 100 = 110 ms of total rendered audio.
154 /// @note Calling `saveOutputTo()` (both for wav files and `AudioBuffer`s) and `savePlotTo()`
155 /// will not include this warm-up section of audio in the output.
156 /// @param warmUpDurationSeconds Duration of the warm‑up in seconds
157 /// @param signalPreparation Whether to call reset() and/or prepare() on an input signal
158 /// after the warm-up stage
159 /// @param dspPreparation Whether to call reset() and/or prepare() on the DSP testee
160 /// after the warm-up stage
162 double warmUpDurationSeconds,
163 Preparation signalPreparation = Preparation::none,
164 Preparation dspPreparation = Preparation::none
165 )
166 {
167 if (warmUpDurationSeconds < 0)
168 HART_THROW_OR_RETURN (hart::ValueError, "Warm-up should be a non-negative value in seconds", *this);
169
170 m_warmUpDurationSeconds = warmUpDurationSeconds;
171 m_signalPreparationAfterWarmUp = signalPreparation;
172 m_dspPreparationAfterWarmUp = dspPreparation;
173 return *this;
174 }
175
176 /// @brief Sets the input signal for the test by copying it
177 /// @param signal Input signal, see @ref Signals
178 AudioTestBuilder& withInputSignal (const SignalBase<SampleType>& signal)
179 {
180 m_inputSignal = std::move (signal.copy());
181 return *this;
182 }
183
184 /// @brief Sets the input signal for the test by moving it
185 /// @param signal Input signal, see @ref Signals
186 AudioTestBuilder& withInputSignal (SignalBase<SampleType>&& signal)
187 {
188 m_inputSignal = std::move (signal.move());
189 return *this;
190 }
191
192 /// @brief Sets the input signal for the test by transfering its smart pointer
193 /// @note The ownership of the smart pointer will be transferred to this class
194 /// @param signal Input signal, see @ref Signals
195 AudioTestBuilder& withInputSignal (std::unique_ptr<SignalBase<SampleType>> signal)
196 {
197 m_inputSignal = std::move (signal);
198 return *this;
199 }
200
201 /// @brief Sets the input signal using a function-based signal definition
202 /// @param signalFunction Function that generates the signal buffer. It will moved to a Signal object.
203 /// @param label Human-readable label for the signal to use in the test error output
204 /// @param loop Determines whether the generated buffer should loop
205 /// @details
206 /// This overload constructs a @ref SignalFunction internally, allowing inline
207 /// definition of signals without explicitly creating a Signal object, for slightly
208 /// less verbose syntax.
209 ///
210 /// The function must have the signature:
211 /// `void (AudioBuffer<SampleType>&)`
212 ///
213 /// This method echoes the ctor of the hart::SignalFunction class, so see its
214 /// documentaion for more detailed description.
215 ///
216 /// @note To re-use the signal made with your function, you can use `saveInputSignalTo()`.
217 /// @see SignalFunction
219 std::function<void (AudioBuffer<SampleType>&)> signalFunction,
220 const std::string& label = {},
221 Loop loop = Loop::yes)
222 {
223 m_inputSignal = hart::make_unique<SignalFunction<SampleType>>(
224 std::move (signalFunction),
225 label,
226 loop
227 );
228
229 return *this;
230 }
231
232 /// @brief Sets arbitrary number of input channels
233 /// @details For common mono and stereo cases, you may use dedicated methods like @ref inStereo() or
234 /// @ref withMonoInput() instead of this one for better readability.
235 /// @param numInputChannels Number of input channels
236 AudioTestBuilder& withInputChannels (size_t numInputChannels)
237 {
238 if (numInputChannels == 0)
239 HART_THROW_OR_RETURN (SizeError, "There should be at least one (mono) audio channel", *this);
240
241 if (numInputChannels > 128)
242 HART_THROW_OR_RETURN (SizeError, "The number of channels is unexpectedly large... Do people really use so many channels?", *this);
243
244 m_numInputChannels = numInputChannels;
245 return *this;
246 }
247
248 /// @brief Sets arbitrary number of output channels
249 /// @details For common mono and stereo cases, you may use dedicated methods like @ref inMono() or
250 /// @ref withStereoOutput() instead of this one for better readability.
251 /// @param numOutputChannels Number of output channels
252 AudioTestBuilder& withOutputChannels (size_t numOutputChannels)
253 {
254 if (numOutputChannels == 0)
255 HART_THROW_OR_RETURN(SizeError, "There should be at least one (mono) audio channel", *this);
256
257 if (numOutputChannels > 128)
258 HART_THROW_OR_RETURN(SizeError, "The number of channels is unexpectedly large... Do people really use so many channels?", *this);
259
260 m_numOutputChannels = numOutputChannels;
261 return *this;
262 }
263
264 /// @brief Sets number of input channels to two
266 {
267 return this->withInputChannels (2);
268 }
269
270 /// @brief Sets number of output channels to two
272 {
273 return this->withOutputChannels (2);
274 }
275
276 /// @brief Sets number of input channels to one
278 {
279 return this->withInputChannels (1);
280 }
281
282 /// @brief Sets number of output channels to one
284 {
285 return this->withOutputChannels (1);
286 }
287
288 /// @brief Sets number of input and output channels to one
290 {
291 return this->withMonoInput().withMonoOutput();
292 }
293
294 /// @brief Sets number of input and output channels to two
296 {
297 return this->withStereoInput().withStereoOutput();
298 }
299
300 /// @brief Adds an "expect" check using a Matcher object
301 /// @param matcher Matcher to perform the check, see @ref Matchers
302 template<typename MatcherType>
303 AudioTestBuilder& expectTrue (MatcherType&& matcher)
304 {
305 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::expect, true);
306 return *this;
307 }
308
309 /// @brief Adds a reversed "expect" check using a Matcher object
310 /// @param matcher Matcher to perform the check, see @ref Matchers
311 template<typename MatcherType>
312 AudioTestBuilder& expectFalse (MatcherType&& matcher)
313 {
314 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::expect, false);
315 return *this;
316 }
317
318 /// @brief Adds an "assert" check using a Matcher object
319 /// @param matcher Matcher to perform the check, see @ref Matchers
320 template<typename MatcherType>
321 AudioTestBuilder& assertTrue (MatcherType&& matcher)
322 {
323 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::assert, true);
324 return *this;
325 }
326
327 /// @brief Adds a reversed "assert" check using a Matcher object
328 /// @param matcher Matcher to perform the check, see @ref Matchers
329 template<typename MatcherType>
330 AudioTestBuilder& assertFalse (MatcherType&& matcher)
331 {
332 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::assert, false);
333 return *this;
334 }
335
336 // TODO: Add expect/assert overloads for smart pointers as well
337
338 /// @brief Adds an "expect" check using a function matcher
339 /// @details Intended for simple inline expressions. For anything more than
340 /// that, consider making a custom hart::Matcher subclass and use it instead.
341 /// @see MatcherFunction
342 /// @param matcherFunction Function with signature:
343 /// @code
344 /// Condition (AnalysisContext<SampleType> context)
345 /// @endcode
346 ///
347 /// Example:
348 /// @code
349 /// [] (AnalysisContext context) { return HART_LESS_THAN (rt60 (context.outputAudio())).get(), 200_ms); }
350 /// @endcode
351 /// @param label Optional label used in failure reports
352 AudioTestBuilder& expectTrue (std::function<Condition (AnalysisContext<SampleType>)> matcherFunction, const std::string& label = {})
353 {
354 return expectTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
355 }
356
357 /// @brief Adds an "expect" check using a function matcher
358 /// @details Intended for simple inline expressions. For anything more than
359 /// that, consider making a custom hart::Matcher subclass and use it instead.
360 /// @see MatcherFunction
361 /// @param matcherFunction Function with signature:
362 /// @code
363 /// Condition (const AudioBuffer<SampleType>& output)
364 /// @endcode
365 ///
366 /// Example:
367 /// @code
368 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
369 /// @endcode
370 /// @param label Optional label used in failure reports
371 AudioTestBuilder& expectTrue (std::function<Condition (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
372 {
373 return expectTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
374 }
375
376 /// @brief Adds an "expect" check using a function matcher
377 /// @details Intended for simple inline expressions. For anything more than
378 /// that, consider making a custom hart::Matcher subclass and use it instead.
379 /// @see MatcherFunction
380 /// @param matcherFunction Function with signature:
381 /// @code
382 /// Condition (const AudioBuffer<SampleType>& input,
383 /// const AudioBuffer<SampleType>& output)
384 /// @endcode
385 ///
386 /// Example:
387 /// @code
388 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
389 /// {
390 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
391 /// }
392 /// @endcode
393 /// @param label Optional label used in failure reports
394 /// @note If your matcher function only cares about the output, and not the input,
395 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
396 AudioTestBuilder& expectTrue (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
397 {
398 return expectTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
399 }
400
401 /// @brief Adds a reversed "expect" check using a function matcher
402 /// @details Intended for simple inline expressions. For anything more than
403 /// that, consider making a custom hart::Matcher subclass and use it instead.
404 /// @see MatcherFunction
405 /// @param matcherFunction Function with signature:
406 /// @code
407 /// Condition (AnalysisContext<SampleType> context)
408 /// @endcode
409 ///
410 /// Example:
411 /// @code
412 /// [] (AnalysisContext context) { return HART_LESS_THAN (rt60 (context.outputAudio())).get(), 200_ms); }
413 /// @endcode
414 /// @param label Optional label used in failure reports
415 AudioTestBuilder& expectFalse (std::function<Condition (AnalysisContext<SampleType>)> matcherFunction, const std::string& label = {})
416 {
417 return expectFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
418 }
419
420 /// @brief Adds a reversed "expect" check using a function matcher
421 /// @details Intended for simple inline expressions. For anything more than
422 /// that, consider making a custom hart::Matcher subclass and use it instead.
423 /// @see MatcherFunction
424 /// @param matcherFunction Function with signature:
425 /// @code
426 /// Condition (const AudioBuffer<SampleType>& output)
427 /// @endcode
428 ///
429 /// Example:
430 /// @code
431 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
432 /// @endcode
433 /// @param label Optional label used in failure reports
434 AudioTestBuilder& expectFalse (std::function<Condition (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
435 {
436 return expectFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
437 }
438
439 /// @brief Adds a reversed "expect" check using a function matcher
440 /// @details Intended for simple inline expressions. For anything more than
441 /// that, consider making a custom hart::Matcher subclass and use it instead.
442 /// @see MatcherFunction
443 /// @param matcherFunction Function with signature:
444 /// @code
445 /// Condition (const AudioBuffer<SampleType>& input,
446 /// const AudioBuffer<SampleType>& output)
447 /// @endcode
448 ///
449 /// Example:
450 /// @code
451 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
452 /// {
453 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
454 /// }
455 /// @endcode
456 /// @param label Optional label used in failure reports
457 /// @note If your matcher function only cares about the output, and not the input,
458 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
459 AudioTestBuilder& expectFalse (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
460 {
461 return expectFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
462 }
463
464 /// @brief Adds an "assert" check using a function matcher
465 /// @details Intended for simple inline expressions. For anything more than
466 /// that, consider making a custom hart::Matcher subclass and use it instead.
467 /// @see MatcherFunction
468 /// @param matcherFunction Function with signature:
469 /// @code
470 /// Condition (AnalysisContext<SampleType> context)
471 /// @endcode
472 ///
473 /// Example:
474 /// @code
475 /// [] (AnalysisContext context) { return HART_LESS_THAN (rt60 (context.outputAudio())).get(), 200_ms); }
476 /// @endcode
477 /// @param label Optional label used in failure reports
478 AudioTestBuilder& assertTrue (std::function<Condition (AnalysisContext<SampleType>)> matcherFunction, const std::string& label = {})
479 {
480 return assertTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
481 }
482
483 /// @brief Adds an "assert" check using a function matcher
484 /// @details Intended for simple inline expressions. For anything more than
485 /// that, consider making a custom hart::Matcher subclass and use it instead.
486 /// @see MatcherFunction
487 /// @param matcherFunction Function with signature:
488 /// @code
489 /// Condition (const AudioBuffer<SampleType>& output)
490 /// @endcode
491 ///
492 /// Example:
493 /// @code
494 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
495 /// @endcode
496 /// @param label Optional label used in failure reports
497 AudioTestBuilder& assertTrue (std::function<Condition (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
498 {
499 return assertTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
500 }
501
502 /// @brief Adds an "assert" check using a function matcher
503 /// @details Intended for simple inline expressions. For anything more than
504 /// that, consider making a custom hart::Matcher subclass and use it instead.
505 /// @see MatcherFunction
506 /// @param matcherFunction Function with signature:
507 /// @code
508 /// Condition (const AudioBuffer<SampleType>& input,
509 /// const AudioBuffer<SampleType>& output)
510 /// @endcode
511 ///
512 /// Example:
513 /// @code
514 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
515 /// {
516 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
517 /// }
518 /// @endcode
519 /// @param label Optional label used in failure reports
520 /// @note If your matcher function only cares about the output, and not the input,
521 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
522 AudioTestBuilder& assertTrue (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
523 {
524 return assertTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
525 }
526
527 /// @brief Adds a reversed "assert" check using a function matcher
528 /// @details Intended for simple inline expressions. For anything more than
529 /// that, consider making a custom hart::Matcher subclass and use it instead.
530 /// @see MatcherFunction
531 /// @param matcherFunction Function with signature:
532 /// @code
533 /// Condition (AnalysisContext<SampleType> context)
534 /// @endcode
535 ///
536 /// Example:
537 /// @code
538 /// [] (AnalysisContext context) { return HART_LESS_THAN (rt60 (context.outputAudio())).get(), 200_ms); }
539 /// @endcode
540 /// @param label Optional label used in failure reports
541 AudioTestBuilder& assertFalse (std::function<Condition (AnalysisContext<SampleType>)> matcherFunction, const std::string& label = {})
542 {
543 return assertFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
544 }
545
546 /// @brief Adds a reversed "assert" check using a function matcher
547 /// @details Intended for simple inline expressions. For anything more than
548 /// that, consider making a custom hart::Matcher subclass and use it instead.
549 /// @see MatcherFunction
550 /// @param matcherFunction Function with signature:
551 /// @code
552 /// Condition (const AudioBuffer<SampleType>& output)
553 /// @endcode
554 ///
555 /// Example:
556 /// @code
557 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
558 /// @endcode
559 /// @param label Optional label used in failure reports
560 AudioTestBuilder& assertFalse (std::function<Condition (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
561 {
562 return assertFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
563 }
564
565 /// @brief Adds a reversed "assert" check using a function matcher
566 /// @details Intended for simple inline expressions. For anything more than
567 /// that, consider making a custom hart::Matcher subclass and use it instead.
568 /// @see MatcherFunction
569 /// @param matcherFunction Function with signature:
570 /// @code
571 /// Condition (const AudioBuffer<SampleType>& input,
572 /// const AudioBuffer<SampleType>& output)
573 /// @endcode
574 ///
575 /// Example:
576 /// @code
577 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
578 /// {
579 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
580 /// }
581 /// @endcode
582 /// @param label Optional label used in failure reports
583 /// @note If your matcher function only cares about the output, and not the input,
584 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
585 AudioTestBuilder& assertFalse (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
586 {
587 return assertFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
588 }
589
590 /// @brief Enables saving input audio to a provided buffer
591 /// @note If you're using `withWarmUp()`, this warm-up section of audio will not be included in the receiving buffer
592 /// @param receivingBuffer A buffer to receive the data. You can pass an unitialised buffer, among other things, as it will be move-assigned.
593 AudioTestBuilder& saveInputTo (AudioBuffer<SampleType>& receivingBuffer)
594 {
595 m_inputBufferSink = [&receivingBuffer] (AudioBuffer<SampleType>&& inputBuffer)
596 {
597 receivingBuffer = std::move (inputBuffer);
598 };
599
600 return *this;
601 }
602
603 /// @brief Enables saving input audio via provided callback
604 /// @note If you're using `withWarmUp()`, this warm-up section of audio will not be included in the receiving
605 /// @param outputBufferSink A callable that accepts a buffer rvalue. The buffer is moved into the provided sink. The test runner takes ownership of the callable object.
606 AudioTestBuilder& saveInputTo (std::function<void (AudioBuffer<SampleType>&&)> inputBufferSink)
607 {
608 m_inputBufferSink = std::move (inputBufferSink);
609 return *this;
610 }
611
612 /// @brief Enables saving output audio to a wav file
613 /// @note If you're using `withWarmUp()`, this warm-up section of audio will not be included in the output file
614 /// @param path File path - relative or absolute. If relative path is set, it will be appended to the provided `--data-root-path` CLI argument.
615 /// @param mode When to save, see @ref hart::Save
616 /// @param wavFormat Format of the wav file, see hart::WavFormat for supported options
617 /// @see HART_REQUIRES_DATA_PATH_ARG
618 AudioTestBuilder& saveOutputTo (const std::string& path, Save mode = Save::always, WavFormat wavFormat = WavFormat::pcm24)
619 {
620 if (path.empty())
621 return *this;
622
623 m_saveOutputPath = toAbsolutePath (path);
624 m_saveOutputMode = mode;
625 m_saveOutputWavFormat = wavFormat;
626 return *this;
627 }
628
629 /// @brief Enables saving output audio to a provided buffer
630 /// @note If you're using `withWarmUp()`, this warm-up section of audio will not be included in the output buffer
631 /// @param receivingBuffer An output buffer to receive the data. You can pass an unitialised buffer, among other things, as it will be move-assigned.
632 AudioTestBuilder& saveOutputTo (AudioBuffer<SampleType>& receivingBuffer)
633 {
634 m_outputBufferSink = [&receivingBuffer] (AudioBuffer<SampleType>&& outputBuffer)
635 {
636 receivingBuffer = std::move (outputBuffer);
637 };
638
639 return *this;
640 }
641
642 /// @brief Enables saving output audio via provided callback
643 /// @note If you're using `withWarmUp()`, this warm-up section of audio will not be included in the output buffer
644 /// @param outputBufferSink A callable that accepts a buffer rvalue. The buffer is moved into the provided sink. The test runner takes ownership of the callable object.
645 AudioTestBuilder& saveOutputTo (std::function<void (AudioBuffer<SampleType>&&)> outputBufferSink)
646 {
647 m_outputBufferSink = std::move (outputBufferSink);
648 return *this;
649 }
650
651 /// @brief Enables saving a plot to an SVG file
652 /// @details This will plot an input and output audio as a waveform
653 /// @note If you're using `withWarmUp()`, this warm-up section of audio will not be included in the plot
654 /// Tip: You can use @ref HART_STR() to construct file names using "<<" syntax.
655 /// @param path File path - relative or absolute. If relative path is set, it will be appended to the provided `--data-root-path` CLI argument.
656 /// @param mode When to save, see @ref hart::Save
657 /// @see HART_REQUIRES_DATA_PATH_ARG
658 AudioTestBuilder& savePlotTo (const std::string& path, Save mode = Save::always)
659 {
660 if (path.empty())
661 return *this;
662
663 m_savePlotPath = toAbsolutePath (path);
664 m_savePlotMode = mode;
665 return *this;
666 }
667
668 /// @brief Moves the input signal after the processing into the provided smart pointer
669 /// @details It's useful if you want to re-use your signal, query it for something,
670 /// or extract some DSP instance from its DSP chain after the test.
671 /// @param receivingSignal A smart pointer that will receive the moved signal
672 AudioTestBuilder& saveInputSignalTo (std::unique_ptr<SignalBase<SampleType>>& receivingSignal)
673 {
674 m_inputSignalSink = [&receivingSignal] (std::unique_ptr<SignalBase<SampleType>>&& usedSignal)
675 {
676 receivingSignal = std::move (usedSignal);
677 };
678
679 return *this;
680 }
681
682 /// @brief Moves the input signal after the processing via provided callback
683 /// @details It's useful if you want to re-use your signal, query it for something,
684 /// or extract some DSP instance from its DSP chain after the test.
685 /// @param inputSignalSink A callable that accepts the moved signal
686 AudioTestBuilder& saveInputSignalTo (std::function<void (std::unique_ptr<SignalBase<SampleType>>&&)> inputSignalSink)
687 {
688 m_inputSignalSink = std::move (inputSignalSink);
689 return *this;
690 }
691
692 /// @brief Adds a label to the test
693 /// @details Useful when you call @ref process() multiple times in one test case - the label
694 /// will be put into test failure report to indicate exactly which test has failed.
695 /// Tip: You can use @ref HART_STR() to construct label strings using "<<" syntax.
696 /// @param testLabel Any text, to be used as a label
697 AudioTestBuilder& withLabel (const std::string& testLabel)
698 {
699 m_testLabel = testLabel;
700 return *this;
701 }
702
703 /// @brief Performs the test
704 /// @details Call this after setting all the test parameters
705 std::unique_ptr<DSPBase<SampleType>> process()
706 {
707 const size_t warmUpDurationFrames = roundToSizeT (m_sampleRateHz * m_warmUpDurationSeconds);
708 const size_t testDurationFrames = roundToSizeT (m_sampleRateHz * m_testDurationSeconds);
709
710 if (testDurationFrames == 0 && warmUpDurationFrames == 0)
711 HART_THROW_OR_RETURN (hart::SizeError, "Nothing to process", std::move (m_processor));
712
713 const bool perBlockChecksPreparationSuccessful = prepareChecks (perBlockChecks);
714 const bool fullSignalChecksPreparationSuccessful = prepareChecks (fullSignalChecks);
715
716 if (! perBlockChecksPreparationSuccessful || ! fullSignalChecksPreparationSuccessful)
717 return std::move (m_processor);
718
719 if (! m_processor->supportsSampleRate (m_sampleRateHz))
720 HART_THROW_OR_RETURN (hart::SampleRateError, "DSP testee does not support requested sample rate", std::move (m_processor));
721
722 if (! m_processor->supportsChannelLayout (m_numInputChannels, m_numOutputChannels))
723 HART_THROW_OR_RETURN (hart::ChannelLayoutError, "DSP testee does not support requested channel layout", std::move (m_processor));
724
725 if (m_dspPreparationBeforeWarmUp == Preparation::reset || m_dspPreparationBeforeWarmUp == Preparation::resetAndPrepare)
726 m_processor->reset();
727
728 if (m_dspPreparationBeforeWarmUp == Preparation::prepare || m_dspPreparationBeforeWarmUp == Preparation::resetAndPrepare)
729 m_processor->prepareWithEnvelopes (m_sampleRateHz, m_numInputChannels, m_numOutputChannels, m_blockSizeFrames);
730
731 for (const ParamValue& paramValue : paramValues)
732 {
733 // TODO: Add true/false return to indicate if setting the parameter was successful
734 m_processor->setValue (paramValue.id, paramValue.value);
735 }
736
737 if (m_inputSignal == nullptr)
738 m_inputSignal = std::move (hart::make_unique<Silence<SampleType>>());
739
740 if (! m_inputSignal->supportsSampleRateWithDSPChain (m_sampleRateHz))
741 HART_THROW_OR_RETURN (hart::SampleRateError, "Input signal or an effect in its DSP chain does not support requested sample rate", std::move (m_processor));
742
743 if (! m_inputSignal->supportsNumChannelsWithDSPChain (m_numInputChannels))
744 HART_THROW_OR_RETURN (hart::ChannelLayoutError, "Input signal or an effect in its DSP chain does not support requested number of channels", std::move (m_processor));
745
746 if (m_signalPreparationBeforeWarmUp == Preparation::reset || m_signalPreparationBeforeWarmUp == Preparation::resetAndPrepare)
747 m_inputSignal->resetWithDSPChain();
748
749 if (m_signalPreparationBeforeWarmUp == Preparation::prepare || m_signalPreparationBeforeWarmUp == Preparation::resetAndPrepare)
750 m_inputSignal->prepareWithDSPChain (m_sampleRateHz, m_numInputChannels, m_blockSizeFrames);
751
752 offsetFrames = 0;
753
754 // Warm-up render
755 while (offsetFrames < warmUpDurationFrames)
756 {
757 const size_t blockSizeFrames = std::min (m_blockSizeFrames, warmUpDurationFrames - offsetFrames);
758
759 hart::AudioBuffer<SampleType> inputBlock (m_numInputChannels, blockSizeFrames, m_sampleRateHz);
760 hart::AudioBuffer<SampleType> outputBlock (m_numOutputChannels, blockSizeFrames, m_sampleRateHz);
761 m_inputSignal->renderNextBlockWithDSPChain (inputBlock);
762 m_processor->processWithEnvelopes (inputBlock, outputBlock);
763
764 offsetFrames += blockSizeFrames;
765 }
766
767 if (m_dspPreparationAfterWarmUp == Preparation::reset || m_dspPreparationAfterWarmUp == Preparation::resetAndPrepare)
768 m_processor->reset();
769
770 if (m_dspPreparationAfterWarmUp == Preparation::prepare || m_dspPreparationAfterWarmUp == Preparation::resetAndPrepare)
771 m_processor->prepareWithEnvelopes (m_sampleRateHz, m_numInputChannels, m_numOutputChannels, m_blockSizeFrames);
772
773 if (m_signalPreparationAfterWarmUp == Preparation::reset || m_signalPreparationAfterWarmUp == Preparation::resetAndPrepare)
774 m_inputSignal->resetWithDSPChain();
775
776 if (m_signalPreparationAfterWarmUp == Preparation::prepare || m_signalPreparationAfterWarmUp == Preparation::resetAndPrepare)
777 m_inputSignal->prepareWithDSPChain (m_sampleRateHz, m_numInputChannels, m_blockSizeFrames);
778
779 AudioBuffer<SampleType> fullInputBuffer (m_numInputChannels, testDurationFrames, m_sampleRateHz);
780 AudioBuffer<SampleType> fullOutputBuffer (m_numOutputChannels, testDurationFrames, m_sampleRateHz);
781 bool atLeastOneCheckFailed = false;
782 offsetFrames = 0;
783
784 // Fill with NaNs for internal post-validation,
785 // to make sure we didn't miss any frames
786 fullInputBuffer.fillWith (nan<SampleType>());
787 fullOutputBuffer.fillWith (nan<SampleType>());
788
789 // Main test render
790 while (offsetFrames < testDurationFrames)
791 {
792 // TODO: Do not continue if there are no checks, or all checks should skip and there's no input and output file to write
793
794 const size_t blockSizeFrames = std::min (m_blockSizeFrames, testDurationFrames - offsetFrames);
795
796 hart::AudioBuffer<SampleType> inputBlock (m_numInputChannels, blockSizeFrames, m_sampleRateHz);
797 hart::AudioBuffer<SampleType> outputBlock (m_numOutputChannels, blockSizeFrames, m_sampleRateHz);
798 m_inputSignal->renderNextBlockWithDSPChain (inputBlock);
799 m_processor->processWithEnvelopes (inputBlock, outputBlock);
800
801 const bool allChecksPassed = processChecks (perBlockChecks, inputBlock, outputBlock, offsetFrames);
802 atLeastOneCheckFailed |= ! allChecksPassed;
803
804 for (size_t channel = 0; channel < m_numInputChannels; ++channel)
805 fullInputBuffer.copyFrom (channel, offsetFrames, inputBlock, channel, 0, blockSizeFrames);
806
807 for (size_t channel = 0; channel < m_numOutputChannels; ++channel)
808 fullOutputBuffer.copyFrom (channel, offsetFrames, outputBlock, channel, 0, blockSizeFrames);
809
810 offsetFrames += blockSizeFrames;
811 }
812
813 // Sanity check - making sure we didn't skip any frames in the input buffer...
814 for (size_t channel = 0; channel < m_numInputChannels; ++channel)
815 {
816 const SampleType* channelData = fullInputBuffer[channel];
817
818 for (size_t frame = 0; frame < testDurationFrames; ++frame)
819 hassert (! std::isnan (channelData[frame]));
820 }
821
822 // ...and the output buffer
823 for (size_t channel = 0; channel < m_numOutputChannels; ++channel)
824 {
825 const SampleType* channelData = fullOutputBuffer[channel];
826
827 for (size_t frame = 0; frame < testDurationFrames; ++frame)
828 hassert (! std::isnan (channelData[frame]));
829 }
830
831 if (testDurationFrames != 0 && ! fullSignalChecks.empty())
832 {
833 const bool allChecksPassed = processChecks (fullSignalChecks, fullInputBuffer, fullOutputBuffer, warmUpDurationFrames);
834 atLeastOneCheckFailed |= ! allChecksPassed;
835 }
836
837 if (m_saveOutputMode == Save::always || (m_saveOutputMode == Save::whenFails && atLeastOneCheckFailed))
838 WavWriter<SampleType>::writeBuffer (fullOutputBuffer, m_saveOutputPath, m_saveOutputWavFormat);
839
840 if (m_savePlotMode == Save::always || (m_savePlotMode == Save::whenFails && atLeastOneCheckFailed))
841 plotData (fullInputBuffer, fullOutputBuffer, m_savePlotPath);
842
843 if (m_inputBufferSink != nullptr)
844 m_inputBufferSink (std::move (fullInputBuffer));
845
846 if (m_outputBufferSink != nullptr)
847 m_outputBufferSink (std::move (fullOutputBuffer));
848
849 if (m_inputSignalSink != nullptr)
850 m_inputSignalSink (std::move (m_inputSignal));
851
852 return std::move (m_processor);
853 }
854
855private:
856 struct ParamValue
857 {
858 int id;
859 double value;
860 };
861
862 enum class SignalAssertionLevel
863 {
864 expect,
865 assert,
866 };
867
868 struct Check
869 {
870 std::unique_ptr<MatcherBase<SampleType>> matcher;
871 SignalAssertionLevel signalAssertionLevel;
872 bool shouldSkip;
873 bool shouldPass;
874 };
875
876 std::unique_ptr<DSPBase<SampleType>> m_processor;
877 std::unique_ptr<SignalBase<SampleType>> m_inputSignal;
882 std::vector<ParamValue> paramValues;
884 double m_warmUpDurationSeconds = 0.0;
885 size_t offsetFrames = 0;
886 std::string m_testLabel = {};
887
888 Preparation m_signalPreparationBeforeWarmUp = Preparation::resetAndPrepare;
889 Preparation m_signalPreparationAfterWarmUp = Preparation::none;
890 Preparation m_dspPreparationBeforeWarmUp = Preparation::resetAndPrepare;
891 Preparation m_dspPreparationAfterWarmUp = Preparation::none;
892
893 std::vector<Check> perBlockChecks;
894 std::vector<Check> fullSignalChecks;
895
896 std::string m_saveOutputPath;
897 Save m_saveOutputMode = Save::never;
898 WavFormat m_saveOutputWavFormat = WavFormat::pcm24;
899
900 std::string m_savePlotPath;
901 Save m_savePlotMode = Save::never;
902
903 std::function<void (AudioBuffer<SampleType>&&)> m_inputBufferSink = nullptr;
904 std::function<void (AudioBuffer<SampleType>&&)> m_outputBufferSink = nullptr;
905 std::function<void (std::unique_ptr<SignalBase<SampleType>>&&)> m_inputSignalSink = nullptr;
906
907 template<
908 typename MatcherType,
909 typename = typename std::enable_if<
910 ! std::is_same<
911 typename std::decay<MatcherType>::type,
912 MatcherBase<SampleType>
913 >::value
914 >::type>
915 void addCheck (MatcherType&& matcher, SignalAssertionLevel assertionLevel, bool shouldPass)
916 {
917 using Derived = typename std::decay<MatcherType>::type;
918 static_assert (std::is_base_of<MatcherBase<SampleType>, Derived>::value, "matcher argument must derive from hart::Matcher");
919
920 const bool forceFullSignal = !shouldPass;
921 auto& group = (matcher.canOperatePerBlock() && !forceFullSignal)
922 ? perBlockChecks
923 : fullSignalChecks;
924
925 // TODO: emplace_back()
926 group.push_back ({
927 std::forward<MatcherType>(matcher).move(),
928 assertionLevel,
929 false,
930 shouldPass
931 });
932 }
933
934 void addCheck (const MatcherBase<SampleType>& matcher, SignalAssertionLevel assertionLevel, bool shouldPass)
935 {
936 const bool forceFullSignal = ! shouldPass;
937 auto& group = (matcher.canOperatePerBlock() && ! forceFullSignal)
938 ? perBlockChecks
939 : fullSignalChecks;
940
941 // TODO: emplace_back()
942 group.push_back({ matcher.copy(), assertionLevel, false, shouldPass });
943 }
944
945 bool prepareChecks (std::vector<Check>& checks)
946 {
947 for (auto& check : checks)
948 {
949 if (! check.matcher->supportsSampleRate (m_sampleRateHz))
950 HART_THROW_OR_RETURN (hart::SampleRateError, "Matcher not support requested sample rate", false);
951
952 if (! check.matcher->supportsChannelLayout (m_numInputChannels, m_numOutputChannels))
953 HART_THROW_OR_RETURN (hart::ChannelLayoutError, "Matcher not support requested number of channels", false);
954
955 check.matcher->prepareWithActiveChannels (m_sampleRateHz, m_numInputChannels, m_numOutputChannels, m_blockSizeFrames);
956 check.shouldSkip = false;
957 }
958
959 return true;
960 }
961
962 bool processChecks (std::vector<Check>& checksGroup, const AudioBuffer<SampleType>& inputAudio, const AudioBuffer<SampleType>& outputAudio, size_t baseFrameOffset)
963 {
964 for (auto& check : checksGroup)
965 {
966 if (check.shouldSkip)
967 continue;
968
969 auto& assertionLevel = check.signalAssertionLevel;
970 auto& matcher = check.matcher;
971
972 const AnalysisContext<SampleType> analysisContext (inputAudio, outputAudio);
973 const bool matchPassed = matcher->match (analysisContext);
974
975 if (matchPassed != check.shouldPass)
976 {
977 check.shouldSkip = true;
978
979 if (assertionLevel == SignalAssertionLevel::assert)
980 {
981 std::stringstream stream;
982 stream << (check.shouldPass ? "assertTrue() failed" : "assertFalse() failed");
983
984 if (! m_testLabel.empty())
985 stream << " at \"" << m_testLabel << "\"";
986
987 stream << std::endl << "Condition: " << *matcher;
988 appendFailureDetails (stream, matcher->getFailureDetails(), inputAudio, outputAudio, baseFrameOffset);
989
990 throw hart::TestAssertException (std::string (stream.str()));
991 }
992 else
993 {
994 std::stringstream stream;
995 stream << (check.shouldPass ? "expectTrue() failed" : "expectFalse() failed");
996
997 if (!m_testLabel.empty())
998 stream << " at \"" << m_testLabel << "\"";
999
1000 stream << std::endl << "Condition: " << * matcher;
1001 appendFailureDetails (stream, matcher->getFailureDetails(), inputAudio, outputAudio, baseFrameOffset);
1002
1003 hart::ExpectationFailureMessages::get().emplace_back (stream.str());
1004 }
1005
1006 // TODO: FIXME: Do not throw inside of per-block loop if requested to write input or output to a wav file, throw after the loop instead
1007 // TODO: Stop processing if expect has failed and outputting to a file wasn't requested
1008 // TODO: Skip all checks if check failed, but asked to output a wav file
1009 return false;
1010 }
1011 }
1012
1013 return true;
1014 }
1015
1016 void appendFailureDetails (std::stringstream& stream, const MatcherFailureDetails& details, const AudioBuffer<SampleType>& inputAudio, const AudioBuffer<SampleType>& observedOutputAudio, size_t baseFrameOffset)
1017 {
1018 const size_t frameOverall = baseFrameOffset + details.frame;
1019 const double timestampOverall = static_cast<double> (frameOverall) / m_sampleRateHz;
1020 const size_t warmUpDurationFrames = (size_t) std::round (m_sampleRateHz * m_warmUpDurationSeconds);
1021 const SampleType inputSampleValue = inputAudio[details.channel][details.frame];
1022 const SampleType outputSampleValue = observedOutputAudio[details.channel][details.frame];
1023
1024 stream << std::endl
1025 << "Input signal: " << *m_inputSignal << std::endl
1026 << "Channel: " << details.channel << std::endl;
1027
1028 if (warmUpDurationFrames == 0)
1029 {
1030 stream
1031 << "Frame: " << frameOverall << std::endl
1032 << secPrecision << "Timestamp: " << timestampOverall << " seconds";
1033 }
1034 else
1035 {
1036 const size_t framePostWarmUp = frameOverall - warmUpDurationFrames;
1037 const double timestampPostWarmUp = static_cast<double> (framePostWarmUp) / m_sampleRateHz;
1038 stream
1039 << "Frame (overall): " << frameOverall << std::endl
1040 << "Frame (post warm-up): " << framePostWarmUp << std::endl
1041 << secPrecision
1042 << "Timestamp (overall): " << timestampOverall << " seconds" << std::endl
1043 << "Timestamp (post warm-up): " << timestampPostWarmUp << " seconds";
1044 }
1045
1046 stream << std::endl
1047 << linPrecision << "Input sample value: " << inputSampleValue
1048 << dbPrecision << " (" << ratioToDecibels (std::abs (inputSampleValue)) << " dB)" << std::endl
1049 << linPrecision << "Output sample value: " << outputSampleValue
1050 << dbPrecision << " (" << ratioToDecibels (std::abs (outputSampleValue)) << " dB)" << std::endl
1051 << details.description;
1052 }
1053};
1054
1055/// @brief Call this to start building your test using a DSP object
1056/// @param dsp Instance of your DSP effect
1057/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
1058/// @ingroup TestRunner
1059template <typename DSPType>
1060AudioTestBuilder<typename std::decay<DSPType>::type::SampleTypePublicAlias> processAudioWith (DSPType&& dsp)
1061{
1062 return AudioTestBuilder<typename std::decay<DSPType>::type::SampleTypePublicAlias> (std::forward<DSPType>(dsp));
1063}
1064
1065/// @brief Call this to start building your test using a smart pointer to a DSP object
1066/// @details Call this for DSP objects that do not support moving or copying
1067/// @param dsp Instance of your DSP effect wrapped in a smart pointer
1068/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
1069/// @ingroup TestRunner
1070template <typename DSPType>
1071AudioTestBuilder<typename DSPType::SampleTypePublicAlias> processAudioWith (std::unique_ptr<DSPType>&& dsp)
1072{
1073 using SampleType = typename DSPType::SampleTypePublicAlias;
1074 return AudioTestBuilder<SampleType> (std::unique_ptr<DSPBase<SampleType>> (dsp.release()));
1075}
1076
1078{
1079/// @brief Call this to start building your test using a sample-wise function
1080/// @details
1081/// This overload allows defining a DSP processor using a function or lambda
1082/// that operates on individual samples.
1083///
1084/// @par Function signature
1085/// @code
1086/// float (float value)
1087/// @endcode
1088///
1089/// The function is applied independently to each sample.
1090///
1091/// For more details, see `DSPFunction` documentation, as it merely forwards
1092/// the arguments to its constructor.
1093///
1094/// @note
1095/// If your DSP requires access to sample rate or channel context,
1096/// consider using one of the block-wise overloads instead.
1097///
1098/// @param dspFunction Function to process each sample.
1099/// @param label Optional human-readable label for error reporting.
1100/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
1101/// @ingroup TestRunner
1102inline AudioTestBuilder<float> processAudioWith (std::function<float (float)> dspFunction, const std::string& label = {})
1103{
1104 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
1105}
1106
1107/// @brief Call this to start building your test using a block-wise in-place function
1108/// @details
1109/// The provided function processes audio in-place. The buffer is pre-filled
1110/// with input data and must be modified directly.
1111///
1112/// @par Function signature
1113/// @code
1114/// void (AudioBuffer<float>& buffer)
1115/// @endcode
1116///
1117/// @par Buffer invariants
1118/// The function must not change:
1119/// - Number of channels
1120/// - Number of frames
1121/// - Sample rate
1122///
1123/// For more details, see `DSPFunction` documentation, as it merely forwards
1124/// the arguments to its constructor.
1125///
1126/// @param dspFunction Function that processes the buffer in-place.
1127/// @param label Optional human-readable label for error reporting.
1128/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
1129/// @ingroup TestRunner
1130inline AudioTestBuilder<float> processAudioWith (std::function<void (AudioBuffer<float>&)> dspFunction, const std::string& label = {})
1131{
1132 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
1133}
1134
1135/// @brief Call this to start building your test using a block-wise non-replacing function
1136/// @details This overload provides separate input and output buffers for processing.
1137///
1138/// @par Function signature
1139/// @code
1140/// void (const AudioBuffer<float>& input,
1141/// AudioBuffer<float>& output)
1142/// @endcode
1143///
1144/// @par Buffer invariants
1145/// The function must not change:
1146/// - Number of channels
1147/// - Number of frames
1148/// - Sample rate
1149///
1150/// For more details, see `DSPFunction` documentation, as it merely forwards
1151/// the arguments to its constructor.
1152///
1153/// @param dspFunction Function that generates output from input.
1154/// @param label Optional human-readable label for error reporting.
1155/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
1156/// @ingroup TestRunner
1157inline AudioTestBuilder<float> processAudioWith (std::function<void (const AudioBuffer<float>&, AudioBuffer<float>&)> dspFunction, const std::string& label = {})
1158{
1159 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
1160}
1161
1162using hart::processAudioWith;
1163
1164} // namespace aliases_float
1165
1167{
1168
1169/// @brief See the description of the `float` version of this function
1170/// @ingroup TestRunner
1171inline AudioTestBuilder<double> processAudioWith (std::function<double (double)> dspFunction, const std::string& label = {})
1172{
1173 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
1174}
1175
1176/// @brief See the description of the `float` version of this function
1177/// @ingroup TestRunner
1178inline AudioTestBuilder<double> processAudioWith (std::function<void (AudioBuffer<double>&)> dspFunction, const std::string& label = {})
1179{
1180 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
1181}
1182
1183/// @brief See the description of the `float` version of this function
1184/// @ingroup TestRunner
1185inline AudioTestBuilder<double> processAudioWith (std::function<void (const AudioBuffer<double>&, AudioBuffer<double>&)> dspFunction, const std::string& label = {})
1186{
1187 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
1188}
1189
1190using hart::processAudioWith;
1191
1192} // namespace aliases_double
1193
1194} // namespace hart
Contains audio-related artefacts useful for analysis by matchers.
A DSP host used for building and running tests inside a test case.
AudioTestBuilder & assertTrue(std::function< Condition(const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds an "assert" check using a function matcher.
AudioTestBuilder & withInputSignal(std::function< void(AudioBuffer< SampleType > &)> signalFunction, const std::string &label={}, Loop loop=Loop::yes)
Sets the input signal using a function-based signal definition.
AudioTestBuilder & expectTrue(std::function< Condition(const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds an "expect" check using a function matcher.
AudioTestBuilder & inMono()
Sets number of input and output channels to one.
AudioTestBuilder & saveInputTo(AudioBuffer< SampleType > &receivingBuffer)
Enables saving input audio to a provided buffer.
AudioTestBuilder & expectFalse(std::function< Condition(const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds a reversed "expect" check using a function matcher.
AudioTestBuilder & withDspPreparation(Preparation dspPreparation)
Sets whether to call reset() and/or prepare() on DSP testee before rendering audio.
AudioTestBuilder & assertFalse(std::function< Condition(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds a reversed "assert" check using a function matcher.
AudioTestBuilder & expectFalse(std::function< Condition(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds a reversed "expect" check using a function matcher.
AudioTestBuilder & assertFalse(std::function< Condition(AnalysisContext< SampleType >)> matcherFunction, const std::string &label={})
Adds a reversed "assert" check using a function matcher.
AudioTestBuilder & withLabel(const std::string &testLabel)
Adds a label to the test.
AudioTestBuilder & withDuration(double durationSeconds)
Sets the total duration of the input signal to be processed.
AudioTestBuilder & withStereoInput()
Sets number of input channels to two.
AudioTestBuilder & expectFalse(MatcherType &&matcher)
Adds a reversed "expect" check using a Matcher object.
AudioTestBuilder & assertFalse(MatcherType &&matcher)
Adds a reversed "assert" check using a Matcher object.
AudioTestBuilder & withInputChannels(size_t numInputChannels)
Sets arbitrary number of input channels.
AudioTestBuilder & expectTrue(std::function< Condition(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds an "expect" check using a function matcher.
AudioTestBuilder & expectTrue(std::function< Condition(AnalysisContext< SampleType >)> matcherFunction, const std::string &label={})
Adds an "expect" check using a function matcher.
AudioTestBuilder(DSPType &&dsp, typename std::enable_if< ! std::is_lvalue_reference< DSPType && >::value &&std::is_base_of< DSPBase< SampleType >, typename std::decay< DSPType >::type >::value >::type *=0)
Moves the DSP instance into the host.
AudioTestBuilder & withSignalPreparation(Preparation signalPreparation)
Sets whether to call reset() and/or prepare() on the input Signal before rendering audio.
AudioTestBuilder(std::unique_ptr< DSPBase< SampleType > > dsp)
Transfers the DSP smart pointer into the host.
AudioTestBuilder & expectTrue(MatcherType &&matcher)
Adds an "expect" check using a Matcher object.
AudioTestBuilder & withSampleRate(double sampleRateHz)
Sets the sample rate for the test.
AudioTestBuilder & withInputSignal(SignalBase< SampleType > &&signal)
Sets the input signal for the test by moving it.
AudioTestBuilder & assertTrue(std::function< Condition(AnalysisContext< SampleType >)> matcherFunction, const std::string &label={})
Adds an "assert" check using a function matcher.
AudioTestBuilder & saveOutputTo(AudioBuffer< SampleType > &receivingBuffer)
Enables saving output audio to a provided buffer.
AudioTestBuilder & withInputSignal(std::unique_ptr< SignalBase< SampleType > > signal)
Sets the input signal for the test by transfering its smart pointer.
AudioTestBuilder & savePlotTo(const std::string &path, Save mode=Save::always)
Enables saving a plot to an SVG file.
AudioTestBuilder & saveInputSignalTo(std::unique_ptr< SignalBase< SampleType > > &receivingSignal)
Moves the input signal after the processing into the provided smart pointer.
AudioTestBuilder & withMonoOutput()
Sets number of output channels to one.
AudioTestBuilder & assertTrue(std::function< Condition(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds an "assert" check using a function matcher.
AudioTestBuilder & assertTrue(MatcherType &&matcher)
Adds an "assert" check using a Matcher object.
AudioTestBuilder & withValue(int id, double value)
Sets the initial param value for the tested DSP.
AudioTestBuilder & assertFalse(std::function< Condition(const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds a reversed "assert" check using a function matcher.
AudioTestBuilder & withWarmUp(double warmUpDurationSeconds, Preparation signalPreparation=Preparation::none, Preparation dspPreparation=Preparation::none)
Adds a warm‑up period before the main test.
AudioTestBuilder & saveOutputTo(const std::string &path, Save mode=Save::always, WavFormat wavFormat=WavFormat::pcm24)
Enables saving output audio to a wav file.
AudioTestBuilder & withInputSignal(const SignalBase< SampleType > &signal)
Sets the input signal for the test by copying it.
AudioTestBuilder & saveInputTo(std::function< void(AudioBuffer< SampleType > &&)> inputBufferSink)
Enables saving input audio via provided callback.
AudioTestBuilder & withMonoInput()
Sets number of input channels to one.
AudioTestBuilder & saveOutputTo(std::function< void(AudioBuffer< SampleType > &&)> outputBufferSink)
Enables saving output audio via provided callback.
AudioTestBuilder & withStereoOutput()
Sets number of output channels to two.
AudioTestBuilder & inStereo()
Sets number of input and output channels to two.
AudioTestBuilder & withOutputChannels(size_t numOutputChannels)
Sets arbitrary number of output channels.
std::unique_ptr< DSPBase< SampleType > > process()
Performs the test.
AudioTestBuilder & withBlockSize(size_t blockSizeFrames)
Sets the block size for the test.
AudioTestBuilder & saveInputSignalTo(std::function< void(std::unique_ptr< SignalBase< SampleType > > &&)> inputSignalSink)
Moves the input signal after the processing via provided callback.
AudioTestBuilder & expectFalse(std::function< Condition(AnalysisContext< SampleType >)> matcherFunction, const std::string &label={})
Adds a reversed "expect" check using a function matcher.
Thrown when a numbers of channels is mismatched.
A class representing some condition.
Polymorphic base for all DSP.
Definition hart_dsp.hpp:33
A DSP processor defined by a user-provided function.
Polymorphic base for all matchers.
Matcher defined by a user-provided function.
Thrown when sample rate is mismatched or invalid.
Signal defined by a user-provided function.
Produces silence (zeros)
Thrown when an unexpected container size is encountered.
Thrown by test asserts like HART_ASSERT_TRUE() and AudioTestBuilder::assertFalse()
Thrown when an inappropriate value is encountered.
Helper class for writing audio buffers to wav files.
#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 ...
std::ostream & linPrecision(std::ostream &stream)
Sets number of decimal places for linear (sample) values.
std::ostream & secPrecision(std::ostream &stream)
Sets number of decimal places for values in seconds.
std::ostream & dbPrecision(std::ostream &stream)
Sets number of decimal places for values in decibels.
AudioTestBuilder< double > processAudioWith(std::function< void(const AudioBuffer< double > &, AudioBuffer< double > &)> dspFunction, const std::string &label={})
See the description of the float version of this function.
ResetSignal
Determines whether to reset the Signal in a given context.
AudioTestBuilder< float > processAudioWith(std::function< void(const AudioBuffer< float > &, AudioBuffer< float > &)> dspFunction, const std::string &label={})
Call this to start building your test using a block-wise non-replacing function.
Save
Determines when to save a file.
AudioTestBuilder< double > processAudioWith(std::function< double(double)> dspFunction, const std::string &label={})
See the description of the float version of this function.
AudioTestBuilder< typename std::decay< DSPType >::type::SampleTypePublicAlias > processAudioWith(DSPType &&dsp)
Call this to start building your test using a DSP object.
AudioTestBuilder< typename DSPType::SampleTypePublicAlias > processAudioWith(std::unique_ptr< DSPType > &&dsp)
Call this to start building your test using a smart pointer to a DSP object.
AudioTestBuilder< float > processAudioWith(std::function< float(float)> dspFunction, const std::string &label={})
Call this to start building your test using a sample-wise function.
AudioTestBuilder< float > processAudioWith(std::function< void(AudioBuffer< float > &)> dspFunction, const std::string &label={})
Call this to start building your test using a block-wise in-place function.
AudioTestBuilder< double > processAudioWith(std::function< void(AudioBuffer< double > &)> dspFunction, const std::string &label={})
See the description of the float version of this function.
@ no
The signal will continue from whatever state it was in.
@ yes
The signal's state will be reset.
@ whenFails
File will be saved only when the test has failed.
@ never
File will not be saved.
@ always
File will be saved always, after the test is performed.
static size_t roundToSizeT(SampleType x)
Rounds a floating point value to a size_t value.
std::unique_ptr< ObjectType > make_unique(Args &&... args)
std::make_unique() replacement for C++11
static std::string toAbsolutePath(const std::string &path)
Converts path to absolute, if it's relative.
Loop
Helper values for something that could loop, like a Signal.
Preparation
Describes whether to call reset() and/or prepare() before rendering through DSP or a Signal.
WavFormat
Audio data storage format for the wav files.
Holds values set by the user via CLI interface.
double getDefaultRenderDurationSeconds() const
size_t getDefaultNumOutputChannels() const
size_t getDefaultNumInputChannels() const
double getDefaultSampleRateHz() const
size_t getGefaultBlockSizeFrames() const
static CLIConfig & getInstance()
Get the singleton instance.
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.