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()
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 always output the entire rendered piece of audio, including this warm-up stage.
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 (const AudioBuffer<SampleType>& output)
345 /// @endcode
346 ///
347 /// Example:
348 /// @code
349 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
350 /// @endcode
351 /// @param label Optional label used in failure reports
352 AudioTestBuilder& expectTrue (std::function<Condition (const AudioBuffer<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>& input,
364 /// const AudioBuffer<SampleType>& output)
365 /// @endcode
366 ///
367 /// Example:
368 /// @code
369 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
370 /// {
371 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
372 /// }
373 /// @endcode
374 /// @param label Optional label used in failure reports
375 /// @note If your matcher function only cares about the output, and not the input,
376 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
377 AudioTestBuilder& expectTrue (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
378 {
379 return expectTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
380 }
381
382 /// @brief Adds a reversed "expect" check using a function matcher
383 /// @details Intended for simple inline expressions. For anything more than
384 /// that, consider making a custom hart::Matcher subclass and use it instead.
385 /// @see MatcherFunction
386 /// @param matcherFunction Function with signature:
387 /// @code
388 /// Condition (const AudioBuffer<SampleType>& output)
389 /// @endcode
390 ///
391 /// Example:
392 /// @code
393 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
394 /// @endcode
395 /// @param label Optional label used in failure reports
396 AudioTestBuilder& expectFalse (std::function<Condition (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
397 {
398 return expectFalse (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 (const AudioBuffer<SampleType>& input,
408 /// const AudioBuffer<SampleType>& output)
409 /// @endcode
410 ///
411 /// Example:
412 /// @code
413 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
414 /// {
415 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
416 /// }
417 /// @endcode
418 /// @param label Optional label used in failure reports
419 /// @note If your matcher function only cares about the output, and not the input,
420 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
421 AudioTestBuilder& expectFalse (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
422 {
423 return expectFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
424 }
425
426 /// @brief Adds an "assert" check using a function matcher
427 /// @details Intended for simple inline expressions. For anything more than
428 /// that, consider making a custom hart::Matcher subclass and use it instead.
429 /// @see MatcherFunction
430 /// @param matcherFunction Function with signature:
431 /// @code
432 /// Condition (const AudioBuffer<SampleType>& output)
433 /// @endcode
434 ///
435 /// Example:
436 /// @code
437 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
438 /// @endcode
439 /// @param label Optional label used in failure reports
440 AudioTestBuilder& assertTrue (std::function<Condition (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
441 {
442 return assertTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
443 }
444
445 /// @brief Adds an "assert" check using a function matcher
446 /// @details Intended for simple inline expressions. For anything more than
447 /// that, consider making a custom hart::Matcher subclass and use it instead.
448 /// @see MatcherFunction
449 /// @param matcherFunction Function with signature:
450 /// @code
451 /// Condition (const AudioBuffer<SampleType>& input,
452 /// const AudioBuffer<SampleType>& output)
453 /// @endcode
454 ///
455 /// Example:
456 /// @code
457 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
458 /// {
459 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
460 /// }
461 /// @endcode
462 /// @param label Optional label used in failure reports
463 /// @note If your matcher function only cares about the output, and not the input,
464 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
465 AudioTestBuilder& assertTrue (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
466 {
467 return assertTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
468 }
469
470 /// @brief Adds a reversed "assert" check using a function matcher
471 /// @details Intended for simple inline expressions. For anything more than
472 /// that, consider making a custom hart::Matcher subclass and use it instead.
473 /// @see MatcherFunction
474 /// @param matcherFunction Function with signature:
475 /// @code
476 /// Condition (const AudioBuffer<SampleType>& output)
477 /// @endcode
478 ///
479 /// Example:
480 /// @code
481 /// [] (const AudioBuffer<SampleType>& output) { return HART_LESS_THAN (crestFactorDb (output), 3_dB); }
482 /// @endcode
483 /// @param label Optional label used in failure reports
484 AudioTestBuilder& assertFalse (std::function<Condition (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
485 {
486 return assertFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
487 }
488
489 /// @brief Adds a reversed "assert" check using a function matcher
490 /// @details Intended for simple inline expressions. For anything more than
491 /// that, consider making a custom hart::Matcher subclass and use it instead.
492 /// @see MatcherFunction
493 /// @param matcherFunction Function with signature:
494 /// @code
495 /// Condition (const AudioBuffer<SampleType>& input,
496 /// const AudioBuffer<SampleType>& output)
497 /// @endcode
498 ///
499 /// Example:
500 /// @code
501 /// [] (const AudioBuffer<SampleType>& input, const AudioBuffer<SampleType>& output)
502 /// {
503 /// return HART_LESS_THAN (crestFactorDb (output), crestFactorDb (input));
504 /// }
505 /// @endcode
506 /// @param label Optional label used in failure reports
507 /// @note If your matcher function only cares about the output, and not the input,
508 /// just use the overload that takes `Condition (const AudioBuffer<SampleType>& output)`.
509 AudioTestBuilder& assertFalse (std::function<Condition (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
510 {
511 return assertFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
512 }
513
514 /// @brief Enables saving output audio to a wav file
515 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the output file
516 /// @param path File path - relative or absolute. If relative path is set, it will be appended to the provided `--data-root-path` CLI argument.
517 /// @param mode When to save, see @ref hart::Save
518 /// @param wavFormat Format of the wav file, see hart::WavFormat for supported options
519 /// @see HART_REQUIRES_DATA_PATH_ARG
520 AudioTestBuilder& saveOutputTo (const std::string& path, Save mode = Save::always, WavFormat wavFormat = WavFormat::pcm24)
521 {
522 if (path.empty())
523 return *this;
524
525 m_saveOutputPath = toAbsolutePath (path);
526 m_saveOutputMode = mode;
527 m_saveOutputWavFormat = wavFormat;
528 return *this;
529 }
530
531 /// @brief Enables saving output audio to a provided buffer
532 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the output buffer
533 /// @details Tip: You can use @ref HART_STR() to construct file names using "<<" syntax.
534 /// @warning The target directory has to exist!
535 /// @param receivingBuffer An output buffer to receive the data. You can pass an unitialised buffer, among other things, as it will be move-assigned.
536 AudioTestBuilder& saveOutputTo (AudioBuffer<SampleType>& receivingBuffer)
537 {
538 m_outputBufferSink = [&receivingBuffer] (AudioBuffer<SampleType>&& outputBuffer)
539 {
540 receivingBuffer = std::move (outputBuffer);
541 };
542
543 return *this;
544 }
545
546 /// @brief Enables saving output audio via provided callback
547 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the output buffer
548 /// @details Tip: You can use @ref HART_STR() to construct file names using "<<" syntax.
549 /// @warning The target directory has to exist!
550 /// @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.
551 AudioTestBuilder& saveOutputTo (std::function<void (AudioBuffer<SampleType>&&)> outputBufferSink)
552 {
553 m_outputBufferSink = std::move (outputBufferSink);
554 return *this;
555 }
556
557 /// @brief Enables saving a plot to an SVG file
558 /// @details This will plot an input and output audio as a waveform
559 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the plot
560 /// Tip: You can use @ref HART_STR() to construct file names using "<<" syntax.
561 /// @param path File path - relative or absolute. If relative path is set, it will be appended to the provided `--data-root-path` CLI argument.
562 /// @param mode When to save, see @ref hart::Save
563 /// @see HART_REQUIRES_DATA_PATH_ARG
564 AudioTestBuilder& savePlotTo (const std::string& path, Save mode = Save::always)
565 {
566 if (path.empty())
567 return *this;
568
569 m_savePlotPath = toAbsolutePath (path);
570 m_savePlotMode = mode;
571 return *this;
572 }
573
574 /// @brief Moves the input signal after the processing into the provided smart pointer
575 /// @details It's useful if you want to re-use your signal, query it for something,
576 /// or extract some DSP instance from its DSP chain after the test.
577 /// @param receivingSignal A smart pointer that will receive the moved signal
578 AudioTestBuilder& saveInputSignalTo (std::unique_ptr<SignalBase<SampleType>>& receivingSignal)
579 {
580 m_inputSignalSink = [&receivingSignal] (std::unique_ptr<SignalBase<SampleType>>&& usedSignal)
581 {
582 receivingSignal = std::move (usedSignal);
583 };
584
585 return *this;
586 }
587
588 /// @brief Moves the input signal after the processing via provided callback
589 /// @details It's useful if you want to re-use your signal, query it for something,
590 /// or extract some DSP instance from its DSP chain after the test.
591 /// @param inputSignalSink A callable that accepts the moved signal
592 AudioTestBuilder& saveInputSignalTo (std::function<void (std::unique_ptr<SignalBase<SampleType>>&&)> inputSignalSink)
593 {
594 m_inputSignalSink = std::move (inputSignalSink);
595 return *this;
596 }
597
598 /// @brief Adds a label to the test
599 /// @details Useful when you call @ref process() multiple times in one test case - the label
600 /// will be put into test failure report to indicate exactly which test has failed.
601 /// Tip: You can use @ref HART_STR() to construct label strings using "<<" syntax.
602 /// @param testLabel Any text, to be used as a label
603 AudioTestBuilder& withLabel (const std::string& testLabel)
604 {
605 m_testLabel = testLabel;
606 return *this;
607 }
608
609 /// @brief Performs the test
610 /// @details Call this after setting all the test parameters
611 std::unique_ptr<DSPBase<SampleType>> process()
612 {
613 const size_t totalDurationFrames = (size_t) std::round (m_sampleRateHz * (m_testDurationSeconds + m_warmUpDurationSeconds));
614 const size_t warmUpDurationFrames = (size_t) std::round (m_sampleRateHz * m_warmUpDurationSeconds);
615 const size_t testDurationFrames = totalDurationFrames - warmUpDurationFrames;
616
617 if (totalDurationFrames == 0)
618 HART_THROW_OR_RETURN (hart::SizeError, "Nothing to process", std::move (m_processor));
619
620 const bool perBlockChecksPreparationSuccessful = prepareChecks (perBlockChecks);
621 const bool fullSignalChecksPreparationSuccessful = prepareChecks (fullSignalChecks);
622
623 if (! perBlockChecksPreparationSuccessful || ! fullSignalChecksPreparationSuccessful)
624 return std::move (m_processor);
625
626 if (! m_processor->supportsSampleRate (m_sampleRateHz))
627 HART_THROW_OR_RETURN (hart::SampleRateError, "DSP testee does not support requested sample rate", std::move (m_processor));
628
629 if (! m_processor->supportsChannelLayout (m_numInputChannels, m_numOutputChannels))
630 HART_THROW_OR_RETURN (hart::ChannelLayoutError, "DSP testee does not support requested channel layout", std::move (m_processor));
631
632 if (m_dspPreparationBeforeWarmUp == Preparation::reset || m_dspPreparationBeforeWarmUp == Preparation::resetAndPrepare)
633 m_processor->reset();
634
635 if (m_dspPreparationBeforeWarmUp == Preparation::prepare || m_dspPreparationBeforeWarmUp == Preparation::resetAndPrepare)
636 m_processor->prepareWithEnvelopes (m_sampleRateHz, m_numInputChannels, m_numOutputChannels, m_blockSizeFrames);
637
638 for (const ParamValue& paramValue : paramValues)
639 {
640 // TODO: Add true/false return to indicate if setting the parameter was successful
641 m_processor->setValue (paramValue.id, paramValue.value);
642 }
643
644 if (m_inputSignal == nullptr)
645 m_inputSignal = std::move (hart::make_unique<Silence<SampleType>>());
646
647 if (! m_inputSignal->supportsSampleRateWithDSPChain (m_sampleRateHz))
648 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));
649
650 if (! m_inputSignal->supportsNumChannelsWithDSPChain (m_numInputChannels))
651 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));
652
653 if (m_signalPreparationBeforeWarmUp == Preparation::reset || m_signalPreparationBeforeWarmUp == Preparation::resetAndPrepare)
654 m_inputSignal->resetWithDSPChain();
655
656 if (m_signalPreparationBeforeWarmUp == Preparation::prepare || m_signalPreparationBeforeWarmUp == Preparation::resetAndPrepare)
657 m_inputSignal->prepareWithDSPChain (m_sampleRateHz, m_numInputChannels, m_blockSizeFrames);
658
659 offsetFrames = 0;
660
661 // TODO: Pre-allocate full buffer sizes here, as they're already known at this point
662 AudioBuffer<SampleType> fullInputBuffer (m_numInputChannels, 0, m_sampleRateHz);
663 AudioBuffer<SampleType> fullOutputBuffer (m_numOutputChannels, 0, m_sampleRateHz);
664 bool atLeastOneCheckFailed = false;
665
666 // Warm-up render
667 while (offsetFrames < warmUpDurationFrames)
668 {
669 const size_t blockSizeFrames = std::min (m_blockSizeFrames, warmUpDurationFrames - offsetFrames);
670
671 hart::AudioBuffer<SampleType> inputBlock (m_numInputChannels, blockSizeFrames, m_sampleRateHz);
672 hart::AudioBuffer<SampleType> outputBlock (m_numOutputChannels, blockSizeFrames, m_sampleRateHz);
673 m_inputSignal->renderNextBlockWithDSPChain (inputBlock);
674 m_processor->processWithEnvelopes (inputBlock, outputBlock);
675
676 fullInputBuffer.appendFrom (inputBlock);
677 fullOutputBuffer.appendFrom (outputBlock);
678
679 offsetFrames += blockSizeFrames;
680 }
681
682 if (m_dspPreparationAfterWarmUp == Preparation::reset || m_dspPreparationAfterWarmUp == Preparation::resetAndPrepare)
683 m_processor->reset();
684
685 if (m_dspPreparationAfterWarmUp == Preparation::prepare || m_dspPreparationAfterWarmUp == Preparation::resetAndPrepare)
686 m_processor->prepareWithEnvelopes (m_sampleRateHz, m_numInputChannels, m_numOutputChannels, m_blockSizeFrames);
687
688 if (m_signalPreparationAfterWarmUp == Preparation::reset || m_signalPreparationAfterWarmUp == Preparation::resetAndPrepare)
689 m_inputSignal->resetWithDSPChain();
690
691 if (m_signalPreparationAfterWarmUp == Preparation::prepare || m_signalPreparationAfterWarmUp == Preparation::resetAndPrepare)
692 m_inputSignal->prepareWithDSPChain (m_sampleRateHz, m_numInputChannels, m_blockSizeFrames);
693
694 // Main test render
695 while (offsetFrames < totalDurationFrames)
696 {
697 // TODO: Do not continue if there are no checks, or all checks should skip and there's no input and output file to write
698 // TODO: Avoid re-allocating inputBlock and outputBlock in every iteration of this loop
699
700 const size_t blockSizeFrames = std::min (m_blockSizeFrames, totalDurationFrames - offsetFrames);
701
702 hart::AudioBuffer<SampleType> inputBlock (m_numInputChannels, blockSizeFrames, m_sampleRateHz);
703 hart::AudioBuffer<SampleType> outputBlock (m_numOutputChannels, blockSizeFrames, m_sampleRateHz);
704 m_inputSignal->renderNextBlockWithDSPChain (inputBlock);
705 m_processor->processWithEnvelopes (inputBlock, outputBlock);
706
707 const bool allChecksPassed = processChecks (perBlockChecks, inputBlock, outputBlock, offsetFrames);
708 atLeastOneCheckFailed |= ! allChecksPassed;
709 fullInputBuffer.appendFrom (inputBlock);
710 fullOutputBuffer.appendFrom (outputBlock);
711
712 offsetFrames += blockSizeFrames;
713 }
714
715 if (testDurationFrames != 0 && ! fullSignalChecks.empty())
716 {
717 // Full audio buffers for full audio matchers
718 // We want to skip the warm-up pieces for those
719 AudioBuffer<SampleType> fullInputNoWarmUpBuffer (m_numInputChannels, testDurationFrames, m_sampleRateHz);
720 AudioBuffer<SampleType> fullOutputNoWarmUpBuffer (m_numOutputChannels, testDurationFrames, m_sampleRateHz);
721
722 for (size_t channel = 0; channel < m_numInputChannels; ++channel)
723 fullInputNoWarmUpBuffer.copyFrom (channel, 0, fullInputBuffer, channel, warmUpDurationFrames, testDurationFrames);
724
725 for (size_t channel = 0; channel < m_numOutputChannels; ++channel)
726 fullOutputNoWarmUpBuffer.copyFrom (channel, 0, fullOutputBuffer, channel, warmUpDurationFrames, testDurationFrames);
727
728 const bool allChecksPassed = processChecks (fullSignalChecks, fullInputNoWarmUpBuffer, fullOutputNoWarmUpBuffer, warmUpDurationFrames);
729 atLeastOneCheckFailed |= ! allChecksPassed;
730 }
731
732 if (m_saveOutputMode == Save::always || (m_saveOutputMode == Save::whenFails && atLeastOneCheckFailed))
733 WavWriter<SampleType>::writeBuffer (fullOutputBuffer, m_saveOutputPath, m_saveOutputWavFormat);
734
735 if (m_savePlotMode == Save::always || (m_savePlotMode == Save::whenFails && atLeastOneCheckFailed))
736 plotData (fullInputBuffer, fullOutputBuffer, m_savePlotPath);
737
738 if (m_outputBufferSink != nullptr)
739 m_outputBufferSink (std::move (fullOutputBuffer));
740
741 if (m_inputSignalSink != nullptr)
742 m_inputSignalSink (std::move (m_inputSignal));
743
744 return std::move (m_processor);
745 }
746
747private:
748 struct ParamValue
749 {
750 int id;
751 double value;
752 };
753
754 enum class SignalAssertionLevel
755 {
756 expect,
757 assert,
758 };
759
760 struct Check
761 {
762 std::unique_ptr<MatcherBase<SampleType>> matcher;
763 SignalAssertionLevel signalAssertionLevel;
764 bool shouldSkip;
765 bool shouldPass;
766 };
767
768 std::unique_ptr<DSPBase<SampleType>> m_processor;
769 std::unique_ptr<SignalBase<SampleType>> m_inputSignal;
774 std::vector<ParamValue> paramValues;
776 double m_warmUpDurationSeconds = 0.0;
777 size_t offsetFrames = 0;
778 std::string m_testLabel = {};
779
780 Preparation m_signalPreparationBeforeWarmUp = Preparation::resetAndPrepare;
781 Preparation m_signalPreparationAfterWarmUp = Preparation::none;
782 Preparation m_dspPreparationBeforeWarmUp = Preparation::resetAndPrepare;
783 Preparation m_dspPreparationAfterWarmUp = Preparation::none;
784
785 std::vector<Check> perBlockChecks;
786 std::vector<Check> fullSignalChecks;
787
788 std::string m_saveOutputPath;
789 Save m_saveOutputMode = Save::never;
790 WavFormat m_saveOutputWavFormat = WavFormat::pcm24;
791
792 std::string m_savePlotPath;
793 Save m_savePlotMode = Save::never;
794
795 std::function<void (AudioBuffer<SampleType>&&)> m_outputBufferSink = nullptr;
796 std::function<void (std::unique_ptr<SignalBase<SampleType>>&&)> m_inputSignalSink = nullptr;
797
798 template<
799 typename MatcherType,
800 typename = typename std::enable_if<
801 ! std::is_same<
802 typename std::decay<MatcherType>::type,
803 MatcherBase<SampleType>
804 >::value
805 >::type>
806 void addCheck (MatcherType&& matcher, SignalAssertionLevel assertionLevel, bool shouldPass)
807 {
808 using Derived = typename std::decay<MatcherType>::type;
809 static_assert (std::is_base_of<MatcherBase<SampleType>, Derived>::value, "matcher argument must derive from hart::Matcher");
810
811 const bool forceFullSignal = !shouldPass;
812 auto& group = (matcher.canOperatePerBlock() && !forceFullSignal)
813 ? perBlockChecks
814 : fullSignalChecks;
815
816 // TODO: emplace_back()
817 group.push_back ({
818 std::forward<MatcherType>(matcher).move(),
819 assertionLevel,
820 false,
821 shouldPass
822 });
823 }
824
825 void addCheck (const MatcherBase<SampleType>& matcher, SignalAssertionLevel assertionLevel, bool shouldPass)
826 {
827 const bool forceFullSignal = ! shouldPass;
828 auto& group = (matcher.canOperatePerBlock() && ! forceFullSignal)
829 ? perBlockChecks
830 : fullSignalChecks;
831
832 // TODO: emplace_back()
833 group.push_back({ matcher.copy(), assertionLevel, false, shouldPass });
834 }
835
836 bool prepareChecks (std::vector<Check>& checks)
837 {
838 for (auto& check : checks)
839 {
840 if (! check.matcher->supportsSampleRate (m_sampleRateHz))
841 HART_THROW_OR_RETURN (hart::SampleRateError, "Matcher not support requested sample rate", false);
842
843 if (! check.matcher->supportsChannelLayout (m_numInputChannels, m_numOutputChannels))
844 HART_THROW_OR_RETURN (hart::ChannelLayoutError, "Matcher not support requested number of channels", false);
845
846 check.matcher->prepareWithActiveChannels (m_sampleRateHz, m_numInputChannels, m_numOutputChannels, m_blockSizeFrames);
847 check.shouldSkip = false;
848 }
849
850 return true;
851 }
852
853 bool processChecks (std::vector<Check>& checksGroup, const AudioBuffer<SampleType>& inputAudio, const AudioBuffer<SampleType>& outputAudio, size_t baseFrameOffset)
854 {
855 for (auto& check : checksGroup)
856 {
857 if (check.shouldSkip)
858 continue;
859
860 auto& assertionLevel = check.signalAssertionLevel;
861 auto& matcher = check.matcher;
862
863 const AnalysisContext<SampleType> analysisContext (inputAudio, outputAudio);
864 const bool matchPassed = matcher->match (analysisContext);
865
866 if (matchPassed != check.shouldPass)
867 {
868 check.shouldSkip = true;
869
870 if (assertionLevel == SignalAssertionLevel::assert)
871 {
872 std::stringstream stream;
873 stream << (check.shouldPass ? "assertTrue() failed" : "assertFalse() failed");
874
875 if (! m_testLabel.empty())
876 stream << " at \"" << m_testLabel << "\"";
877
878 stream << std::endl << "Condition: " << *matcher;
879 appendFailureDetails (stream, matcher->getFailureDetails(), inputAudio, outputAudio, baseFrameOffset);
880
881 throw hart::TestAssertException (std::string (stream.str()));
882 }
883 else
884 {
885 std::stringstream stream;
886 stream << (check.shouldPass ? "expectTrue() failed" : "expectFalse() failed");
887
888 if (!m_testLabel.empty())
889 stream << " at \"" << m_testLabel << "\"";
890
891 stream << std::endl << "Condition: " << * matcher;
892 appendFailureDetails (stream, matcher->getFailureDetails(), inputAudio, outputAudio, baseFrameOffset);
893
894 hart::ExpectationFailureMessages::get().emplace_back (stream.str());
895 }
896
897 // 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
898 // TODO: Stop processing if expect has failed and outputting to a file wasn't requested
899 // TODO: Skip all checks if check failed, but asked to output a wav file
900 return false;
901 }
902 }
903
904 return true;
905 }
906
907 void appendFailureDetails (std::stringstream& stream, const MatcherFailureDetails& details, const AudioBuffer<SampleType>& inputAudio, const AudioBuffer<SampleType>& observedOutputAudio, size_t baseFrameOffset)
908 {
909 const size_t frameOverall = baseFrameOffset + details.frame;
910 const double timestampOverall = static_cast<double> (frameOverall) / m_sampleRateHz;
911 const size_t warmUpDurationFrames = (size_t) std::round (m_sampleRateHz * m_warmUpDurationSeconds);
912 const SampleType inputSampleValue = inputAudio[details.channel][details.frame];
913 const SampleType outputSampleValue = observedOutputAudio[details.channel][details.frame];
914
915 stream << std::endl
916 << "Input signal: " << *m_inputSignal << std::endl
917 << "Channel: " << details.channel << std::endl;
918
919 if (warmUpDurationFrames == 0)
920 {
921 stream
922 << "Frame: " << frameOverall << std::endl
923 << secPrecision << "Timestamp: " << timestampOverall << " seconds";
924 }
925 else
926 {
927 const size_t framePostWarmUp = frameOverall - warmUpDurationFrames;
928 const double timestampPostWarmUp = static_cast<double> (framePostWarmUp) / m_sampleRateHz;
929 stream
930 << "Frame (overall): " << frameOverall << std::endl
931 << "Frame (post warm-up): " << framePostWarmUp << std::endl
932 << secPrecision
933 << "Timestamp (overall): " << timestampOverall << " seconds" << std::endl
934 << "Timestamp (post warm-up): " << timestampPostWarmUp << " seconds";
935 }
936
937 stream << std::endl
938 << linPrecision << "Input sample value: " << inputSampleValue
939 << dbPrecision << " (" << ratioToDecibels (std::abs (inputSampleValue)) << " dB)" << std::endl
940 << linPrecision << "Output sample value: " << outputSampleValue
941 << dbPrecision << " (" << ratioToDecibels (std::abs (outputSampleValue)) << " dB)" << std::endl
942 << details.description;
943 }
944};
945
946/// @brief Call this to start building your test using a DSP object
947/// @param dsp Instance of your DSP effect
948/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
949/// @ingroup TestRunner
950template <typename DSPType>
951AudioTestBuilder<typename std::decay<DSPType>::type::SampleTypePublicAlias> processAudioWith (DSPType&& dsp)
952{
953 return AudioTestBuilder<typename std::decay<DSPType>::type::SampleTypePublicAlias> (std::forward<DSPType>(dsp));
954}
955
956/// @brief Call this to start building your test using a smart pointer to a DSP object
957/// @details Call this for DSP objects that do not support moving or copying
958/// @param dsp Instance of your DSP effect wrapped in a smart pointer
959/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
960/// @ingroup TestRunner
961template <typename DSPType>
962AudioTestBuilder<typename DSPType::SampleTypePublicAlias> processAudioWith (std::unique_ptr<DSPType>&& dsp)
963{
964 using SampleType = typename DSPType::SampleTypePublicAlias;
965 return AudioTestBuilder<SampleType> (std::unique_ptr<DSPBase<SampleType>> (dsp.release()));
966}
967
969{
970/// @brief Call this to start building your test using a sample-wise function
971/// @details
972/// This overload allows defining a DSP processor using a function or lambda
973/// that operates on individual samples.
974///
975/// @par Function signature
976/// @code
977/// float (float value)
978/// @endcode
979///
980/// The function is applied independently to each sample.
981///
982/// For more details, see `DSPFunction` documentation, as it merely forwards
983/// the arguments to its constructor.
984///
985/// @note
986/// If your DSP requires access to sample rate or channel context,
987/// consider using one of the block-wise overloads instead.
988///
989/// @param dspFunction Function to process each sample.
990/// @param label Optional human-readable label for error reporting.
991/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
992/// @ingroup TestRunner
993inline AudioTestBuilder<float> processAudioWith (std::function<float (float)> dspFunction, const std::string& label = {})
994{
995 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
996}
997
998/// @brief Call this to start building your test using a block-wise in-place function
999/// @details
1000/// The provided function processes audio in-place. The buffer is pre-filled
1001/// with input data and must be modified directly.
1002///
1003/// @par Function signature
1004/// @code
1005/// void (AudioBuffer<float>& buffer)
1006/// @endcode
1007///
1008/// @par Buffer invariants
1009/// The function must not change:
1010/// - Number of channels
1011/// - Number of frames
1012/// - Sample rate
1013///
1014/// For more details, see `DSPFunction` documentation, as it merely forwards
1015/// the arguments to its constructor.
1016///
1017/// @param dspFunction Function that processes the buffer in-place.
1018/// @param label Optional human-readable label for error reporting.
1019/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
1020/// @ingroup TestRunner
1021inline AudioTestBuilder<float> processAudioWith (std::function<void (AudioBuffer<float>&)> dspFunction, const std::string& label = {})
1022{
1023 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
1024}
1025
1026/// @brief Call this to start building your test using a block-wise non-replacing function
1027/// @details This overload provides separate input and output buffers for processing.
1028///
1029/// @par Function signature
1030/// @code
1031/// void (const AudioBuffer<float>& input,
1032/// AudioBuffer<float>& output)
1033/// @endcode
1034///
1035/// @par Buffer invariants
1036/// The function must not change:
1037/// - Number of channels
1038/// - Number of frames
1039/// - Sample rate
1040///
1041/// For more details, see `DSPFunction` documentation, as it merely forwards
1042/// the arguments to its constructor.
1043///
1044/// @param dspFunction Function that generates output from input.
1045/// @param label Optional human-readable label for error reporting.
1046/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
1047/// @ingroup TestRunner
1048inline AudioTestBuilder<float> processAudioWith (std::function<void (const AudioBuffer<float>&, AudioBuffer<float>&)> dspFunction, const std::string& label = {})
1049{
1050 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
1051}
1052
1053using hart::processAudioWith;
1054
1055} // namespace aliases_float
1056
1058{
1059
1060/// @brief See the description of the `float` version of this function
1061/// @ingroup TestRunner
1062inline AudioTestBuilder<double> processAudioWith (std::function<double (double)> dspFunction, const std::string& label = {})
1063{
1064 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
1065}
1066
1067/// @brief See the description of the `float` version of this function
1068/// @ingroup TestRunner
1069inline AudioTestBuilder<double> processAudioWith (std::function<void (AudioBuffer<double>&)> dspFunction, const std::string& label = {})
1070{
1071 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
1072}
1073
1074/// @brief See the description of the `float` version of this function
1075/// @ingroup TestRunner
1076inline AudioTestBuilder<double> processAudioWith (std::function<void (const AudioBuffer<double>&, AudioBuffer<double>&)> dspFunction, const std::string& label = {})
1077{
1078 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
1079}
1080
1081using hart::processAudioWith;
1082
1083} // namespace aliases_double
1084
1085} // namespace hart
Contains audio-related artefacts useful for analysis by matchers.
Container for audio data.
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 & 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 & 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(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 & 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 & 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.
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.
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 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.
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.