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
13#include "dsp/hart_dsp_all.hpp"
14#include "dsp/hart_dsp_function.hpp"
16#include "matchers/hart_matcher.hpp"
17#include "matchers/hart_matcher_function.hpp"
18#include "hart_plot.hpp"
21#include "signals/hart_signals_all.hpp"
22#include "hart_utils.hpp" // make_unique()
23
24namespace hart {
25
26/// @defgroup TestRunner Test Runner
27/// @brief Runs the tests
28
29/// @brief Determines when to save a file
30/// @ingroup TestRunner
31enum class Save
32{
33 always, ///< File will be saved always, after the test is performed
34 whenFails, ///< File will be saved only when the test has failed
35 never ///< File will not be saved
36};
37
38/// @brief Determines whether to reset the Signal in a given context
39/// @ingroup TestRunner
40enum class ResetSignal
41{
42 no, ///< The signal will continue from whatever state it was in
43 yes ///< The signal's state will be reset
44};
45
46/// @brief A DSP host used for building and running tests inside a test case
47/// @ingroup TestRunner
48template <typename SampleType>
50{
51public:
52 /// @brief Moves the DSP instance into the host
53 /// @details DSP instance will be moved into this host, and then returned by @ref process(), so you can re-use it.
54 /// You can only pass a DSP by moving it, since some of the custom DSP wrappers can be non-copyable.
55 /// If you do want to copy a DSP instance here, use its DSPBase::copy() method explicitly.
56 /// @param dsp Your DSP instance
57 template <typename DSPType>
58 AudioTestBuilder (DSPType&& dsp,
59 typename std::enable_if<
60 ! std::is_lvalue_reference<DSPType&&>::value &&
61 std::is_base_of<DSPBase<SampleType>, typename std::decay<DSPType>::type>::value
62 >::type* = 0)
63 : m_processor (std::forward<DSPType> (dsp).move())
64 {
65 }
66
67 /// @brief Transfers the DSP smart pointer into the host
68 /// @details Use this if your DSP does not support copying or moving. It will be owned by this host,
69 /// and then returned by @ref process(), so you can re-use it.
70 /// @param dsp A smart pointer to your DSP instance
71 AudioTestBuilder (std::unique_ptr<DSPBase<SampleType>> dsp)
72 : m_processor (std::move (dsp))
73 {
74 }
75
76 /// @brief Sets the sample rate for the test
77 /// @details All the signals, effects and sub hosts are guaranteed to be initialized to this sample rate
78 /// @param sampleRateHz Sample rate in Hz. You can use frequency-related literails from @ref Units.
79 AudioTestBuilder& withSampleRate (double sampleRateHz)
80 {
81 if (sampleRateHz <= 0)
82 HART_THROW_OR_RETURN (hart::ValueError, "Sample rate should be a positive value in Hz", *this);
83
84 if (! m_processor->supportsSampleRate (sampleRateHz))
85 HART_THROW_OR_RETURN (hart::SampleRateError, "Sample rate is not supported by the tested DSP", *this);
86
87 m_sampleRateHz = sampleRateHz;
88 return *this;
89 }
90
91 /// @brief Sets the block size for the test
92 /// @param blockSizeFrames Block size in frames (samples)
93 AudioTestBuilder& withBlockSize (size_t blockSizeFrames)
94 {
95 if (blockSizeFrames == 0)
96 HART_THROW_OR_RETURN (hart::SizeError, "Illegal block size - should be a positive value in frames (samples)", *this);
97
98 m_blockSizeFrames = blockSizeFrames;
99 return *this;
100 }
101
102 /// @brief Sets the initial param value for the tested DSP
103 /// @details It will call @ref DSP::setValue() for DSP under test
104 /// @param id Parameter ID (see @ref DSP::setValue())
105 /// @param value Value that needs to be set
106 AudioTestBuilder& withValue (int id, double value)
107 {
108 // TODO: Handle cases when processor already has an envelope for this id
109 paramValues.emplace_back (ParamValue { id, value });
110 return *this;
111 }
112
113 /// @brief Sets the total duration of the input signal to be processed
114 /// @param durationSeconds of the signal in seconds. You can use time-related literails from @ref Units.
115 AudioTestBuilder& withDuration (double durationSeconds)
116 {
117 if (durationSeconds < 0)
118 HART_THROW_OR_RETURN(hart::ValueError, "Signal duration should be a non-negative value in seconds", *this);
119
120 m_testDurationSeconds = durationSeconds;
121 return *this;
122 }
123
124 /// @brief Adds a warm‑up period before the main test.
125 /// @details The signal will be processed for this time, but no matchers will be invoked.
126 /// This can be useful if your DSP uses parameter smoothers internally, that need to settle
127 /// before performing the test, or has some sort of attack envelope stage, like a compressor,
128 /// that you want to skip. This time will be added up with a regular test render run, i.e.
129 /// `processAudioWith (...).withDuration (100_ms).withWarmUp (10_ms)` will result in
130 /// 10 + 100 = 110 ms of total rendered audio.
131 /// @note Calling `saveOutputTo()` (both for wav files and `AudioBuffer`s) and `savePlotTo()`
132 /// will always output the entire rendered piece of audio, including this warm-up stage.
133 /// @param warmUpDurationSeconds Duration of the warm‑up in seconds
134 /// @param resetSignalAfterWarmUp Whether to restart the input signal generator after the warm‑up stage,
135 /// see @ref hart::ResetSignal
136 AudioTestBuilder& withWarmUp (double warmUpDurationSeconds, ResetSignal resetSignalAfterWarmUp = ResetSignal::no)
137 {
138 if (warmUpDurationSeconds < 0)
139 HART_THROW_OR_RETURN (hart::ValueError, "Warm-up should be a non-negative value in seconds", *this);
140
141 m_warmUpDurationSeconds = warmUpDurationSeconds;
142 m_resetSignalAfterWarmUp = resetSignalAfterWarmUp == ResetSignal::yes;
143 return *this;
144 }
145
146 /// @brief Sets the input signal for the test by copying it
147 /// @param signal Input signal, see @ref Signals
148 /// @param resetSignalBeforeProcessing Set to `ResetSignal::yes` if you want the runner to call
149 /// Signal::reset() before rendering audio, or to `ResetSignal::no` to keep its pre-existing state
150 AudioTestBuilder& withInputSignal (const SignalBase<SampleType>& signal, ResetSignal resetSignalBeforeProcessing = ResetSignal::no)
151 {
152 m_inputSignal = std::move (signal.copy());
153 m_resetSignalBeforeProcessing = resetSignalBeforeProcessing == ResetSignal::yes;
154 return *this;
155 }
156
157 /// @brief Sets the input signal for the test by moving it
158 /// @param signal Input signal, see @ref Signals
159 /// @param resetSignalBeforeProcessing Set to `ResetSignal::yes` if you want the runner to call
160 /// Signal::reset() before rendering audio, or to `ResetSignal::no` to keep its pre-existing state
161 AudioTestBuilder& withInputSignal (SignalBase<SampleType>&& signal, ResetSignal resetSignalBeforeProcessing = ResetSignal::no)
162 {
163 m_inputSignal = std::move (signal.move());
164 m_resetSignalBeforeProcessing = resetSignalBeforeProcessing == ResetSignal::yes;
165 return *this;
166 }
167
168 /// @brief Sets the input signal for the test by transfering its smart pointer
169 /// @note The ownership of the smart pointer will be transferred to this class
170 /// @param signal Input signal, see @ref Signals
171 /// @param resetSignalBeforeProcessing Set to `ResetSignal::yes` if you want the runner to call
172 /// Signal::reset() before rendering audio, or to `ResetSignal::no` to keep its pre-existing state
173 AudioTestBuilder& withInputSignal (std::unique_ptr<SignalBase<SampleType>> signal, ResetSignal resetSignalBeforeProcessing = ResetSignal::no)
174 {
175 m_inputSignal = std::move (signal);
176 m_resetSignalBeforeProcessing = resetSignalBeforeProcessing == ResetSignal::yes;
177 return *this;
178 }
179
180 /// @brief Sets the input signal using a function-based signal definition
181 /// @param signalFunction Function that generates the signal buffer. It will moved to a Signal object.
182 /// @param label Human-readable label for the signal to use in the test error output
183 /// @param loop Determines whether the generated buffer should loop
184 /// @details
185 /// This overload constructs a @ref SignalFunction internally, allowing inline
186 /// definition of signals without explicitly creating a Signal object, for slightly
187 /// less verbose syntax.
188 ///
189 /// The function must have the signature:
190 /// `void (AudioBuffer<SampleType>&)`
191 ///
192 /// This method echoes the ctor of the hart::SignalFunction class, so see its
193 /// documentaion for more detailed description.
194 ///
195 /// @note To re-use the signal made with your function, you can use `saveInputSignalTo()`.
196 /// @see SignalFunction
198 std::function<void (AudioBuffer<SampleType>&)> signalFunction,
199 const std::string& label = {},
200 Loop loop = Loop::yes)
201 {
202 m_inputSignal = hart::make_unique<SignalFunction<SampleType>>(
203 std::move (signalFunction),
204 label,
205 loop
206 );
207
208 m_resetSignalBeforeProcessing = false; // It gets constructed from scratch here anyway
209 return *this;
210 }
211
212 /// @brief Sets arbitrary number of input channels
213 /// @details For common mono and stereo cases, you may use dedicated methods like @ref inStereo() or
214 /// @ref withMonoInput() instead of this one for better readability.
215 /// @param numInputChannels Number of input channels
216 AudioTestBuilder& withInputChannels (size_t numInputChannels)
217 {
218 if (numInputChannels == 0)
219 HART_THROW_OR_RETURN (SizeError, "There should be at least one (mono) audio channel", *this);
220
221 if (numInputChannels > 128)
222 HART_THROW_OR_RETURN (SizeError, "The number of channels is unexpectedly large... Do people really use so many channels?", *this);
223
224 m_numInputChannels = numInputChannels;
225 return *this;
226 }
227
228 /// @brief Sets arbitrary number of output channels
229 /// @details For common mono and stereo cases, you may use dedicated methods like @ref inMono() or
230 /// @ref withStereoOutput() instead of this one for better readability.
231 /// @param numOutputChannels Number of output channels
232 AudioTestBuilder& withOutputChannels (size_t numOutputChannels)
233 {
234 if (numOutputChannels == 0)
235 HART_THROW_OR_RETURN(SizeError, "There should be at least one (mono) audio channel", *this);
236
237 if (numOutputChannels > 128)
238 HART_THROW_OR_RETURN(SizeError, "The number of channels is unexpectedly large... Do people really use so many channels?", *this);
239
240 m_numOutputChannels = numOutputChannels;
241 return *this;
242 }
243
244 /// @brief Sets number of input channels to two
246 {
247 return this->withInputChannels (2);
248 }
249
250 /// @brief Sets number of output channels to two
252 {
253 return this->withOutputChannels (2);
254 }
255
256 /// @brief Sets number of input channels to one
258 {
259 return this->withInputChannels (1);
260 }
261
262 /// @brief Sets number of output channels to one
264 {
265 return this->withOutputChannels (1);
266 }
267
268 /// @brief Sets number of input and output channels to one
270 {
271 return this->withMonoInput().withMonoOutput();
272 }
273
274 /// @brief Sets number of input and output channels to two
276 {
277 return this->withStereoInput().withStereoOutput();
278 }
279
280 /// @brief Adds an "expect" check using a Matcher object
281 /// @param matcher Matcher to perform the check, see @ref Matchers
282 template<typename MatcherType>
283 AudioTestBuilder& expectTrue (MatcherType&& matcher)
284 {
285 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::expect, true);
286 return *this;
287 }
288
289 /// @brief Adds a reversed "expect" check using a Matcher object
290 /// @param matcher Matcher to perform the check, see @ref Matchers
291 template<typename MatcherType>
292 AudioTestBuilder& expectFalse (MatcherType&& matcher)
293 {
294 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::expect, false);
295 return *this;
296 }
297
298 /// @brief Adds an "assert" check using a Matcher object
299 /// @param matcher Matcher to perform the check, see @ref Matchers
300 template<typename MatcherType>
301 AudioTestBuilder& assertTrue (MatcherType&& matcher)
302 {
303 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::assert, true);
304 return *this;
305 }
306
307 /// @brief Adds a reversed "assert" check using a Matcher object
308 /// @param matcher Matcher to perform the check, see @ref Matchers
309 template<typename MatcherType>
310 AudioTestBuilder& assertFalse (MatcherType&& matcher)
311 {
312 addCheck (std::forward<MatcherType> (matcher), SignalAssertionLevel::assert, false);
313 return *this;
314 }
315
316 // TODO: Add expect/assert overloads for smart pointers as well
317
318 /// @brief Adds an "expect" check using a function matcher
319 /// @details Intended for simple inline expressions. For anything more than
320 /// that, consider making a custom hart::Matcher subclass and use it instead.
321 /// @see MatcherFunction
322 /// @param matcherFunction Function with signature:
323 /// @code
324 /// bool(const AudioBuffer<SampleType>& output)
325 /// @endcode
326 /// @param label Optional label used in failure reports
327 AudioTestBuilder& expectTrue (std::function<bool (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
328 {
329 return expectTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
330 }
331
332 /// @brief Adds an "expect" check using a function matcher
333 /// @details Intended for simple inline expressions. For anything more than
334 /// that, consider making a custom hart::Matcher subclass and use it instead.
335 /// @see MatcherFunction
336 /// @param matcherFunction Function with signature:
337 /// @code
338 /// bool(const AudioBuffer<SampleType>& input,
339 /// const AudioBuffer<SampleType>& output)
340 /// @endcode
341 /// @param label Optional label used in failure reports
342 /// @note If your matcher function only cares about the output, and not the input,
343 /// just use the overload that takes `bool(const AudioBuffer<SampleType>& output)`.
344 AudioTestBuilder& expectTrue (std::function<bool (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
345 {
346 return expectTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
347 }
348
349 /// @brief Adds a reversed "expect" check using a function matcher
350 /// @details Intended for simple inline expressions. For anything more than
351 /// that, consider making a custom hart::Matcher subclass and use it instead.
352 /// @see MatcherFunction
353 /// @param matcherFunction Function with signature:
354 /// @code
355 /// bool(const AudioBuffer<SampleType>& output)
356 /// @endcode
357 /// @param label Optional label used in failure reports
358 AudioTestBuilder& expectFalse (std::function<bool (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
359 {
360 return expectFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
361 }
362
363 /// @brief Adds a reversed "expect" check using a function matcher
364 /// @details Intended for simple inline expressions. For anything more than
365 /// that, consider making a custom hart::Matcher subclass and use it instead.
366 /// @see MatcherFunction
367 /// @param matcherFunction Function with signature:
368 /// @code
369 /// bool(const AudioBuffer<SampleType>& input,
370 /// const AudioBuffer<SampleType>& output)
371 /// @endcode
372 /// @param label Optional label used in failure reports
373 /// @note If your matcher function only cares about the output, and not the input,
374 /// just use the overload that takes `bool(const AudioBuffer<SampleType>& output)`.
375 AudioTestBuilder& expectFalse (std::function<bool (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
376 {
377 return expectFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
378 }
379
380 /// @brief Adds an "assert" check using a function matcher
381 /// @details Intended for simple inline expressions. For anything more than
382 /// that, consider making a custom hart::Matcher subclass and use it instead.
383 /// @see MatcherFunction
384 /// @param matcherFunction Function with signature:
385 /// @code
386 /// bool(const AudioBuffer<SampleType>& output)
387 /// @endcode
388 /// @param label Optional label used in failure reports
389 AudioTestBuilder& assertTrue (std::function<bool (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
390 {
391 return assertTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
392 }
393
394 /// @brief Adds an "assert" check using a function matcher
395 /// @details Intended for simple inline expressions. For anything more than
396 /// that, consider making a custom hart::Matcher subclass and use it instead.
397 /// @see MatcherFunction
398 /// @param matcherFunction Function with signature:
399 /// @code
400 /// bool(const AudioBuffer<SampleType>& input,
401 /// const AudioBuffer<SampleType>& output)
402 /// @endcode
403 /// @param label Optional label used in failure reports
404 /// @note If your matcher function only cares about the output, and not the input,
405 /// just use the overload that takes `bool(const AudioBuffer<SampleType>& output)`.
406 AudioTestBuilder& assertTrue (std::function<bool (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
407 {
408 return assertTrue (MatcherFunction<SampleType> (std::move (matcherFunction), label));
409 }
410
411 /// @brief Adds a reversed "assert" check using a function matcher
412 /// @details Intended for simple inline expressions. For anything more than
413 /// that, consider making a custom hart::Matcher subclass and use it instead.
414 /// @see MatcherFunction
415 /// @param matcherFunction Function with signature:
416 /// @code
417 /// bool(const AudioBuffer<SampleType>& output)
418 /// @endcode
419 /// @param label Optional label used in failure reports
420 AudioTestBuilder& assertFalse (std::function<bool (const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
421 {
422 return assertFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
423 }
424
425 /// @brief Adds a reversed "assert" check using a function matcher
426 /// @details Intended for simple inline expressions. For anything more than
427 /// that, consider making a custom hart::Matcher subclass and use it instead.
428 /// @see MatcherFunction
429 /// @param matcherFunction Function with signature:
430 /// @code
431 /// bool(const AudioBuffer<SampleType>& input,
432 /// const AudioBuffer<SampleType>& output)
433 /// @endcode
434 /// @param label Optional label used in failure reports
435 /// @note If your matcher function only cares about the output, and not the input,
436 /// just use the overload that takes `bool(const AudioBuffer<SampleType>& output)`.
437 AudioTestBuilder& assertFalse (std::function<bool (const AudioBuffer<SampleType>&, const AudioBuffer<SampleType>&)> matcherFunction, const std::string& label = {})
438 {
439 return assertFalse (MatcherFunction<SampleType> (std::move (matcherFunction), label));
440 }
441
442 /// @brief Enables saving output audio to a wav file
443 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the output file
444 /// @param path File path - relative or absolute. If relative path is set, it will be appended to the provided `--data-root-path` CLI argument.
445 /// @param mode When to save, see @ref hart::Save
446 /// @param wavFormat Format of the wav file, see hart::WavFormat for supported options
447 /// @see HART_REQUIRES_DATA_PATH_ARG
448 AudioTestBuilder& saveOutputTo (const std::string& path, Save mode = Save::always, WavFormat wavFormat = WavFormat::pcm24)
449 {
450 if (path.empty())
451 return *this;
452
453 m_saveOutputPath = toAbsolutePath (path);
454 m_saveOutputMode = mode;
455 m_saveOutputWavFormat = wavFormat;
456 return *this;
457 }
458
459 /// @brief Enables saving output audio to a provided buffer
460 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the output buffer
461 /// @details Tip: You can use @ref HART_STR() to construct file names using "<<" syntax.
462 /// @warning The target directory has to exist!
463 /// @param receivingBuffer An output buffer to receive the data. You can pass an unitialised buffer, among other things, as it will be move-assigned.
464 AudioTestBuilder& saveOutputTo (AudioBuffer<SampleType>& receivingBuffer)
465 {
466 m_outputBufferSink = [&receivingBuffer] (AudioBuffer<SampleType>&& outputBuffer)
467 {
468 receivingBuffer = std::move (outputBuffer);
469 };
470
471 return *this;
472 }
473
474 /// @brief Enables saving output audio via provided callback
475 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the output buffer
476 /// @details Tip: You can use @ref HART_STR() to construct file names using "<<" syntax.
477 /// @warning The target directory has to exist!
478 /// @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.
479 AudioTestBuilder& saveOutputTo (std::function<void (AudioBuffer<SampleType>&&)> outputBufferSink)
480 {
481 m_outputBufferSink = std::move (outputBufferSink);
482 return *this;
483 }
484
485 /// @brief Enables saving a plot to an SVG file
486 /// @details This will plot an input and output audio as a waveform
487 /// @note If you're using `withWarmUp()`, this warm-up section of audio will also be included in the plot
488 /// Tip: You can use @ref HART_STR() to construct file names using "<<" syntax.
489 /// @param path File path - relative or absolute. If relative path is set, it will be appended to the provided `--data-root-path` CLI argument.
490 /// @param mode When to save, see @ref hart::Save
491 /// @see HART_REQUIRES_DATA_PATH_ARG
492 AudioTestBuilder& savePlotTo (const std::string& path, Save mode = Save::always)
493 {
494 if (path.empty())
495 return *this;
496
497 m_savePlotPath = toAbsolutePath (path);
498 m_savePlotMode = mode;
499 return *this;
500 }
501
502 /// @brief Moves the input signal after the processing into the provided smart pointer
503 /// @details It's useful if you want to re-use your signal, query it for something,
504 /// or extract some DSP instance from its DSP chain after the test.
505 /// @param receivingSignal A smart pointer that will receive the moved signal
506 AudioTestBuilder& saveInputSignalTo (std::unique_ptr<SignalBase<SampleType>>& receivingSignal)
507 {
508 m_inputSignalSink = [&receivingSignal] (std::unique_ptr<SignalBase<SampleType>>&& usedSignal)
509 {
510 receivingSignal = std::move (usedSignal);
511 };
512
513 return *this;
514 }
515
516 /// @brief Moves the input signal after the processing via provided callback
517 /// @details It's useful if you want to re-use your signal, query it for something,
518 /// or extract some DSP instance from its DSP chain after the test.
519 /// @param inputSignalSink A callable that accepts the moved signal
520 AudioTestBuilder& saveInputSignalTo (std::function<void (std::unique_ptr<SignalBase<SampleType>>&&)> inputSignalSink)
521 {
522 m_inputSignalSink = std::move (inputSignalSink);
523 return *this;
524 }
525
526 /// @brief Adds a label to the test
527 /// @details Useful when you call @ref process() multiple times in one test case - the label
528 /// will be put into test failure report to indicate exactly which test has failed.
529 /// Tip: You can use @ref HART_STR() to construct label strings using "<<" syntax.
530 /// @param testLabel Any text, to be used as a label
531 AudioTestBuilder& withLabel (const std::string& testLabel)
532 {
533 m_testLabel = testLabel;
534 return *this;
535 }
536
537 /// @brief Perfoems the test
538 /// @details Call this after setting all the test parameters
539 std::unique_ptr<DSPBase<SampleType>> process()
540 {
541 const size_t totalDurationFrames = (size_t) std::round (m_sampleRateHz * (m_testDurationSeconds + m_warmUpDurationSeconds));
542 const size_t warmUpDurationFrames = (size_t) std::round (m_sampleRateHz * m_warmUpDurationSeconds);
543 const size_t testDurationFrames = totalDurationFrames - warmUpDurationFrames;
544
545 if (totalDurationFrames == 0)
546 HART_THROW_OR_RETURN (hart::SizeError, "Nothing to process", std::move (m_processor));
547
548 for (auto& check : perBlockChecks)
549 {
550 check.matcher->prepare (m_sampleRateHz, m_numOutputChannels, m_blockSizeFrames);
551 check.shouldSkip = false;
552 }
553
554 for (auto& check : fullSignalChecks)
555 {
556 check.matcher->prepare (m_sampleRateHz, m_numOutputChannels, m_blockSizeFrames);
557 check.shouldSkip = false;
558 }
559
560 // TODO: Ckeck supportsChannelLayout() here
561 m_processor->reset();
562 m_processor->prepareWithEnvelopes (m_sampleRateHz, m_numInputChannels, m_numOutputChannels, m_blockSizeFrames);
563
564 for (const ParamValue& paramValue : paramValues)
565 {
566 // TODO: Add true/false return to indicate if setting the parameter was successful
567 m_processor->setValue (paramValue.id, paramValue.value);
568 }
569
570 if (m_inputSignal == nullptr)
571 HART_THROW_OR_RETURN (hart::StateError, "No input signal - call withInputSignal() first!", std::move (m_processor));
572
573 if (m_resetSignalBeforeProcessing)
574 m_inputSignal->resetWithDSPChain();
575
576 m_inputSignal->prepareWithDSPChain (m_sampleRateHz, m_numInputChannels, m_blockSizeFrames);
577 offsetFrames = 0;
578
579 // TODO: Pre-allocate full buffer sizes here, as they're already known at this point
580 AudioBuffer<SampleType> fullInputBuffer (m_numInputChannels, 0, m_sampleRateHz);
581 AudioBuffer<SampleType> fullOutputBuffer (m_numOutputChannels, 0, m_sampleRateHz);
582 bool atLeastOneCheckFailed = false;
583
584 // Warm-up render
585 while (offsetFrames < warmUpDurationFrames)
586 {
587 const size_t blockSizeFrames = std::min (m_blockSizeFrames, warmUpDurationFrames - offsetFrames);
588
589 hart::AudioBuffer<SampleType> inputBlock (m_numInputChannels, blockSizeFrames, m_sampleRateHz);
590 hart::AudioBuffer<SampleType> outputBlock (m_numOutputChannels, blockSizeFrames, m_sampleRateHz);
591 m_inputSignal->renderNextBlockWithDSPChain (inputBlock);
592 m_processor->processWithEnvelopes (inputBlock, outputBlock);
593
594 fullInputBuffer.appendFrom (inputBlock);
595 fullOutputBuffer.appendFrom (outputBlock);
596
597 offsetFrames += blockSizeFrames;
598 }
599
600 if (m_resetSignalAfterWarmUp)
601 m_inputSignal->resetWithDSPChain();
602
603 // Main test render
604 while (offsetFrames < totalDurationFrames)
605 {
606 // TODO: Do not continue if there are no checks, or all checks should skip and there's no input and output file to write
607
608 const size_t blockSizeFrames = std::min (m_blockSizeFrames, totalDurationFrames - offsetFrames);
609
610 hart::AudioBuffer<SampleType> inputBlock (m_numInputChannels, blockSizeFrames, m_sampleRateHz);
611 hart::AudioBuffer<SampleType> outputBlock (m_numOutputChannels, blockSizeFrames, m_sampleRateHz);
612 m_inputSignal->renderNextBlockWithDSPChain (inputBlock);
613 m_processor->processWithEnvelopes (inputBlock, outputBlock);
614
615 const bool allChecksPassed = processChecks (perBlockChecks, inputBlock, outputBlock, offsetFrames);
616 atLeastOneCheckFailed |= ! allChecksPassed;
617 fullInputBuffer.appendFrom (inputBlock);
618 fullOutputBuffer.appendFrom (outputBlock);
619
620 offsetFrames += blockSizeFrames;
621 }
622
623 if (testDurationFrames != 0 && ! fullSignalChecks.empty())
624 {
625 // Full audio buffers for full audio matchers
626 // We want to skip the warm-up pieces for those
627 AudioBuffer<SampleType> fullInputNoWarmUpBuffer (m_numInputChannels, testDurationFrames, m_sampleRateHz);
628 AudioBuffer<SampleType> fullOutputNoWarmUpBuffer (m_numOutputChannels, testDurationFrames, m_sampleRateHz);
629
630 for (size_t channel = 0; channel < m_numInputChannels; ++channel)
631 fullInputNoWarmUpBuffer.copyFrom (channel, 0, fullInputBuffer, channel, warmUpDurationFrames, testDurationFrames);
632
633 for (size_t channel = 0; channel < m_numOutputChannels; ++channel)
634 fullOutputNoWarmUpBuffer.copyFrom (channel, 0, fullOutputBuffer, channel, warmUpDurationFrames, testDurationFrames);
635
636 const bool allChecksPassed = processChecks (fullSignalChecks, fullInputNoWarmUpBuffer, fullOutputNoWarmUpBuffer, warmUpDurationFrames);
637 atLeastOneCheckFailed |= ! allChecksPassed;
638 }
639
640 if (m_saveOutputMode == Save::always || (m_saveOutputMode == Save::whenFails && atLeastOneCheckFailed))
641 WavWriter<SampleType>::writeBuffer (fullOutputBuffer, m_saveOutputPath, m_saveOutputWavFormat);
642
643 if (m_savePlotMode == Save::always || (m_savePlotMode == Save::whenFails && atLeastOneCheckFailed))
644 plotData (fullInputBuffer, fullOutputBuffer, m_savePlotPath);
645
646 if (m_outputBufferSink != nullptr)
647 m_outputBufferSink (std::move (fullOutputBuffer));
648
649 if (m_inputSignalSink != nullptr)
650 m_inputSignalSink (std::move (m_inputSignal));
651
652 return std::move (m_processor);
653 }
654
655private:
656 struct ParamValue
657 {
658 int id;
659 double value;
660 };
661
662 enum class SignalAssertionLevel
663 {
664 expect,
665 assert,
666 };
667
668 struct Check
669 {
670 std::unique_ptr<MatcherBase<SampleType>> matcher;
671 SignalAssertionLevel signalAssertionLevel;
672 bool shouldSkip;
673 bool shouldPass;
674 };
675
676 std::unique_ptr<DSPBase<SampleType>> m_processor;
677 std::unique_ptr<SignalBase<SampleType>> m_inputSignal;
678 double m_sampleRateHz = (double) 44100;
679 size_t m_blockSizeFrames = 1024;
680 size_t m_numInputChannels = 1;
681 size_t m_numOutputChannels = 1;
682 std::vector<ParamValue> paramValues;
683 double m_testDurationSeconds = 0.1;
684 double m_warmUpDurationSeconds = 0.0;
685 bool m_resetSignalAfterWarmUp = false;
686 bool m_resetSignalBeforeProcessing = false;
687 size_t offsetFrames = 0;
688 std::string m_testLabel = {};
689
690 std::vector<Check> perBlockChecks;
691 std::vector<Check> fullSignalChecks;
692
693 std::string m_saveOutputPath;
694 Save m_saveOutputMode = Save::never;
695 WavFormat m_saveOutputWavFormat = WavFormat::pcm24;
696
697 std::string m_savePlotPath;
698 Save m_savePlotMode = Save::never;
699
700 std::function<void (AudioBuffer<SampleType>&&)> m_outputBufferSink = nullptr;
701 std::function<void (std::unique_ptr<SignalBase<SampleType>>&&)> m_inputSignalSink = nullptr;
702
703 template<
704 typename MatcherType,
705 typename = typename std::enable_if<
706 ! std::is_same<
707 typename std::decay<MatcherType>::type,
708 MatcherBase<SampleType>
709 >::value
710 >::type>
711 void addCheck (MatcherType&& matcher, SignalAssertionLevel assertionLevel, bool shouldPass)
712 {
713 using Derived = typename std::decay<MatcherType>::type;
714 static_assert (std::is_base_of<MatcherBase<SampleType>, Derived>::value, "matcher argument must derive from hart::Matcher");
715
716 const bool forceFullSignal = !shouldPass;
717 auto& group = (matcher.canOperatePerBlock() && !forceFullSignal)
718 ? perBlockChecks
719 : fullSignalChecks;
720
721 // TODO: emplace_back()
722 group.push_back ({
723 std::forward<MatcherType>(matcher).move(),
724 assertionLevel,
725 false,
726 shouldPass
727 });
728 }
729
730 void addCheck (const MatcherBase<SampleType>& matcher, SignalAssertionLevel assertionLevel, bool shouldPass)
731 {
732 const bool forceFullSignal = ! shouldPass;
733 auto& group = (matcher.canOperatePerBlock() && ! forceFullSignal)
734 ? perBlockChecks
735 : fullSignalChecks;
736
737 // TODO: emplace_back()
738 group.push_back({ matcher.copy(), assertionLevel, false, shouldPass });
739 }
740
741 bool processChecks (std::vector<Check>& checksGroup, const AudioBuffer<SampleType>& inputAudio, const AudioBuffer<SampleType>& outputAudio, size_t baseFrameOffset)
742 {
743 for (auto& check : checksGroup)
744 {
745 if (check.shouldSkip)
746 continue;
747
748 auto& assertionLevel = check.signalAssertionLevel;
749 auto& matcher = check.matcher;
750
751 const bool matchPassed = matcher->match (inputAudio, outputAudio);
752
753 if (matchPassed != check.shouldPass)
754 {
755 check.shouldSkip = true;
756
757 if (assertionLevel == SignalAssertionLevel::assert)
758 {
759 std::stringstream stream;
760 stream << (check.shouldPass ? "assertTrue() failed" : "assertFalse() failed");
761
762 if (! m_testLabel.empty())
763 stream << " at \"" << m_testLabel << "\"";
764
765 stream << std::endl << "Condition: " << *matcher;
766
767 if (check.shouldPass)
768 appendFailureDetails (stream, matcher->getFailureDetails(), inputAudio, outputAudio, baseFrameOffset);
769
770 throw hart::TestAssertException (std::string (stream.str()));
771 }
772 else
773 {
774 std::stringstream stream;
775 stream << (check.shouldPass ? "expectTrue() failed" : "expectFalse() failed");
776
777 if (!m_testLabel.empty())
778 stream << " at \"" << m_testLabel << "\"";
779
780 stream << std::endl << "Condition: " << * matcher;
781
782 if (check.shouldPass)
783 appendFailureDetails (stream, matcher->getFailureDetails(), inputAudio, outputAudio, baseFrameOffset);
784
785 hart::ExpectationFailureMessages::get().emplace_back (stream.str());
786 }
787
788 // 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
789 // TODO: Stop processing if expect has failed and outputting to a file wasn't requested
790 // TODO: Skip all checks if check failed, but asked to output a wav file
791 return false;
792 }
793 }
794
795 return true;
796 }
797
798 void appendFailureDetails (std::stringstream& stream, const MatcherFailureDetails& details, const AudioBuffer<SampleType>& inputAudio, const AudioBuffer<SampleType>& observedOutputAudio, size_t baseFrameOffset)
799 {
800 // TODO: Display input sample info as well
801
802 const size_t frameOverall = baseFrameOffset + details.frame;
803 const double timestampOverall = static_cast<double> (frameOverall) / m_sampleRateHz;
804 const size_t warmUpDurationFrames = (size_t) std::round (m_sampleRateHz * m_warmUpDurationSeconds);
805 const SampleType inputSampleValue = inputAudio[details.channel][details.frame];
806 const SampleType outputSampleValue = observedOutputAudio[details.channel][details.frame];
807
808 stream << std::endl
809 << "Input signal: " << *m_inputSignal << std::endl
810 << "Channel: " << details.channel << std::endl;
811
812 if (warmUpDurationFrames == 0)
813 {
814 stream
815 << "Frame: " << frameOverall << std::endl
816 << secPrecision << "Timestamp: " << timestampOverall << " seconds";
817 }
818 else
819 {
820 const size_t framePostWarmUp = frameOverall - warmUpDurationFrames;
821 const double timestampPostWarmUp = static_cast<double> (framePostWarmUp) / m_sampleRateHz;
822 stream
823 << "Frame (overall): " << frameOverall << std::endl
824 << "Frame (post warm-up): " << framePostWarmUp << std::endl
825 << secPrecision
826 << "Timestamp (overall): " << timestampOverall << " seconds" << std::endl
827 << "Timestamp (post warm-up): " << timestampPostWarmUp << " seconds";
828 }
829
830 stream << std::endl
831 << linPrecision << "Input sample value: " << inputSampleValue
832 << dbPrecision << " (" << ratioToDecibels (std::abs (inputSampleValue)) << " dB)" << std::endl
833 << linPrecision << "Output sample value: " << outputSampleValue
834 << dbPrecision << " (" << ratioToDecibels (std::abs (outputSampleValue)) << " dB)" << std::endl
835 << details.description;
836 }
837};
838
839/// @brief Call this to start building your test using a DSP object
840/// @param dsp Instance of your DSP effect
841/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
842/// @ingroup TestRunner
843template <typename DSPType>
844AudioTestBuilder<typename std::decay<DSPType>::type::SampleTypePublicAlias> processAudioWith (DSPType&& dsp)
845{
846 return AudioTestBuilder<typename std::decay<DSPType>::type::SampleTypePublicAlias> (std::forward<DSPType>(dsp));
847}
848
849/// @brief Call this to start building your test using a smart pointer to a DSP object
850/// @details Call this for DSP objects that do not support moving or copying
851/// @param dsp Instance of your DSP effect wrapped in a smart pointer
852/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
853/// @ingroup TestRunner
854template <typename DSPType>
855AudioTestBuilder<typename DSPType::SampleTypePublicAlias> processAudioWith (std::unique_ptr<DSPType>&& dsp)
856{
857 using SampleType = typename DSPType::SampleTypePublicAlias;
858 return AudioTestBuilder<SampleType> (std::unique_ptr<DSPBase<SampleType>> (dsp.release()));
859}
860
862{
863/// @brief Call this to start building your test using a sample-wise function
864/// @details
865/// This overload allows defining a DSP processor using a function or lambda
866/// that operates on individual samples.
867///
868/// @par Function signature
869/// @code
870/// float (float value)
871/// @endcode
872///
873/// The function is applied independently to each sample.
874///
875/// For more details, see `DSPFunction` documentation, as it merely forwards
876/// the arguments to its constructor.
877///
878/// @note
879/// If your DSP requires access to sample rate or channel context,
880/// consider using one of the block-wise overloads instead.
881///
882/// @param dspFunction Function to process each sample.
883/// @param label Optional human-readable label for error reporting.
884/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
885/// @ingroup TestRunner
886inline AudioTestBuilder<float> processAudioWith (std::function<float (float)> dspFunction, const std::string& label = {})
887{
888 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
889}
890
891/// @brief Call this to start building your test using a block-wise in-place function
892/// @details
893/// The provided function processes audio in-place. The buffer is pre-filled
894/// with input data and must be modified directly.
895///
896/// @par Function signature
897/// @code
898/// void (AudioBuffer<float>& buffer)
899/// @endcode
900///
901/// @par Buffer invariants
902/// The function must not change:
903/// - Number of channels
904/// - Number of frames
905/// - Sample rate
906///
907/// For more details, see `DSPFunction` documentation, as it merely forwards
908/// the arguments to its constructor.
909///
910/// @param dspFunction Function that processes the buffer in-place.
911/// @param label Optional human-readable label for error reporting.
912/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
913/// @ingroup TestRunner
914inline AudioTestBuilder<float> processAudioWith (std::function<void (AudioBuffer<float>&)> dspFunction, const std::string& label = {})
915{
916 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
917}
918
919/// @brief Call this to start building your test using a block-wise non-replacing function
920/// @details This overload provides separate input and output buffers for processing.
921///
922/// @par Function signature
923/// @code
924/// void (const AudioBuffer<float>& input,
925/// AudioBuffer<float>& output)
926/// @endcode
927///
928/// @par Buffer invariants
929/// The function must not change:
930/// - Number of channels
931/// - Number of frames
932/// - Sample rate
933///
934/// For more details, see `DSPFunction` documentation, as it merely forwards
935/// the arguments to its constructor.
936///
937/// @param dspFunction Function that generates output from input.
938/// @param label Optional human-readable label for error reporting.
939/// @return @ref AudioTestBuilder instance - you can chain a bunch of test parameters with it.
940/// @ingroup TestRunner
941inline AudioTestBuilder<float> processAudioWith (std::function<void (const AudioBuffer<float>&, AudioBuffer<float>&)> dspFunction, const std::string& label = {})
942{
943 return AudioTestBuilder<float> (hart::make_unique<hart::DSPFunction<float>> (std::move (dspFunction), label));
944}
945
946using hart::processAudioWith;
947
948} // namespace aliases_float
949
951{
952
953/// @brief See the description of the `float` version of this function
954/// @ingroup TestRunner
955inline AudioTestBuilder<double> processAudioWith (std::function<double (double)> dspFunction, const std::string& label = {})
956{
957 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
958}
959
960/// @brief See the description of the `float` version of this function
961/// @ingroup TestRunner
962inline AudioTestBuilder<double> processAudioWith (std::function<void (AudioBuffer<double>&)> dspFunction, const std::string& label = {})
963{
964 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
965}
966
967/// @brief See the description of the `float` version of this function
968/// @ingroup TestRunner
969inline AudioTestBuilder<double> processAudioWith (std::function<void (const AudioBuffer<double>&, AudioBuffer<double>&)> dspFunction, const std::string& label = {})
970{
971 return AudioTestBuilder<double> (hart::make_unique<hart::DSPFunction<double>> (std::move (dspFunction), label));
972}
973
974using hart::processAudioWith;
975
976} // namespace aliases_double
977
978} // namespace hart
Container for audio data.
A DSP host used for building and running tests inside a test case.
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 & withInputSignal(SignalBase< SampleType > &&signal, ResetSignal resetSignalBeforeProcessing=ResetSignal::no)
Sets the input signal for the test by moving it.
AudioTestBuilder & inMono()
Sets number of input and output channels to one.
AudioTestBuilder & assertFalse(std::function< bool(const AudioBuffer< SampleType > &, const AudioBuffer< 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 & expectTrue(std::function< bool(const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds an "expect" check using a function matcher.
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(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 & assertFalse(std::function< bool(const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds a reversed "assert" check using a function matcher.
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 & assertTrue(std::function< bool(const AudioBuffer< 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 & savePlotTo(const std::string &path, Save mode=Save::always)
Enables saving a plot to an SVG file.
AudioTestBuilder & withInputSignal(const SignalBase< SampleType > &signal, ResetSignal resetSignalBeforeProcessing=ResetSignal::no)
Sets the input signal for the test by copying it.
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 & expectFalse(std::function< bool(const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds a reversed "expect" check using a function matcher.
AudioTestBuilder & expectFalse(std::function< bool(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds a reversed "expect" 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 & expectTrue(std::function< bool(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds an "expect" check using a function matcher.
AudioTestBuilder & saveOutputTo(const std::string &path, Save mode=Save::always, WavFormat wavFormat=WavFormat::pcm24)
Enables saving output audio to a wav file.
AudioTestBuilder & withMonoInput()
Sets number of input channels to one.
AudioTestBuilder & withWarmUp(double warmUpDurationSeconds, ResetSignal resetSignalAfterWarmUp=ResetSignal::no)
Adds a warm‑up period before the main test.
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 & assertTrue(std::function< bool(const AudioBuffer< SampleType > &, const AudioBuffer< SampleType > &)> matcherFunction, const std::string &label={})
Adds an "assert" check using a function matcher.
AudioTestBuilder & withOutputChannels(size_t numOutputChannels)
Sets arbitrary number of output channels.
std::unique_ptr< DSPBase< SampleType > > process()
Perfoems 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 & withInputSignal(std::unique_ptr< SignalBase< SampleType > > signal, ResetSignal resetSignalBeforeProcessing=ResetSignal::no)
Sets the input signal for the test by transfering its smart pointer.
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.
Polymorphic base for all signals.
Signal defined by a user-provided function.
Thrown when an unexpected container size is encountered.
Thrown when some unexpected state 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.
WavFormat
Audio data storage format for the wav files.
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.