HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_dsp.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // fill()
4#include <memory>
5#include <ostream>
6#include <string>
7#include <unordered_map>
8
11#include "envelopes/hart_envelope.hpp"
12#include "hart_utils.hpp" // make_unique()
13
14namespace hart
15{
16
17/// @defgroup DSP DSP
18/// @brief Process signals
19
20/// @brief Hash table of automation envelope sequences mapped to param ids.
21/// @details Keys: Param IDs (int enums like GainDb::gainDb)
22/// Values: Sequence of automation envelope values for this Param ID, one value per frame
23/// @ingroup DataStructures
24using EnvelopeBuffers = std::unordered_map<int, std::vector<double>>;
25
26/// @brief Polymorphic base for all DSP
27/// @warning This class exists only for type erasure and polymorphism.
28/// Do NOT inherit custom DSP from this class directly.
29/// Inherit from @ref hart::DSP instead.
30/// @ingroup DSP
31template <typename SampleType>
33{
34public:
35 /// @brief Prepare for processing
36 /// @details In real-time DSP, such methods are usually used for allocating memory and other non-realtime-safe and heavyweight
37 /// operations. But keep in mind that that HART does not do real-time processing, so this merely follows common real-time
38 /// DSP design conventions, where non-realtime operations are done in a separate callback like this one.
39 /// This method is guaranteed to be called after @ref supportsChannelLayout() and @ref supportsSampleRate(), but before @ref process().
40 /// It is guaranteed that the number of input and output channels obeys supportsChannelLayout() and supportsSampleRate() preferences.
41 /// It is guaranteed that all subsequent process() calls will be in line with the arguments received in this callback.
42 /// @param sampleRateHz sample rate at which the audio should be interpreted and processed
43 /// @param numInputChannels Number of input channels
44 /// @param numOutputChannels Number of output channels
45 /// @param maxBlockSizeFrames Maximum block size in frames (samples)
46 virtual void prepare (double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames) = 0;
47
48 /// @brief Processes the audio
49 /// @details Depending on circumstances, this callback will either be called once to process an entire piece of audio from
50 /// start to finish, or called repeatedly, one block at a time (see @ref AudioTestBuilder::withBlockSize()).
51 /// All audio blocks except the last one are guaranteed to be equal to ```maxBlockSizeFrames``` set in @ref prepare() callback.
52 /// It is guaranteed that input and output buffers are equal in length in frames (samples) to each,
53 /// but they might have different number of channels. Use @ref supportsChannelLayout() to indicate
54 /// whether the effect supports a specific i/o configuration or not, as it will be called before @ref prepare().
55 /// It is guaranteed that ```envelopeBuffers``` will only contain the values for all attached envelopes for this instance of DSP
56 /// effect, and will not contain any data (including key with empty item) if there's no envelope attached to a specific parameter
57 /// ID in this effects's instance. It will never contain envelopes for IDs that get rejected by @ref supportsEnvelopeFor().
58 /// Each vector in the `envelopeBuffers` map is guaranteed to be at least as large as input (and output) block. For partial blocks,
59 /// only the first `input.getNumFrames()` elements contain valid data for the current block; the rest may be stale or uninitialized.
60 /// DSP implementations are expected to ignore values beyond that index range.
61 /// @note This method may be called in a replacing manner, i. e. ```input``` and ```output``` may be references to the same object.
62 /// @warning Remember that the very last block of audio is almost always smaller than the block size set in @ref prepare(), so be
63 /// careful with buffer bounds.
64 /// @note Note that this method does not have to be real-time safe, as all rendering always happens offline.
65 /// Also note that, unlike real-time audio applications, this method is called on the same thread as all others like @ref prepare().
66 /// @param input Input audio block
67 /// @param output Output audio block
68 /// @param envelopeBuffers Envelope values for this block, see @ref EnvelopeBuffers
69 /// @param channelsToProcess Channels that need processing. Process channels that are marked `true`, bypass ones marked `false`.
70 virtual void process (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output, const EnvelopeBuffers& envelopeBuffers, ChannelFlags channelsToProcess) = 0;
71
72 /// @brief Resets to initial state
73 /// @details Ideally should be implemented in a way that audio produced after resetting is identical to audio produced after instantiation
74 virtual void reset() {}
75
76 /// @brief Sets DSP value
77 /// @param paramId Some ID that your subclass understands;
78 /// use of enums is encouraged for readability
79 /// @param value Value of the param in an appropriate unit;
80 /// use of SI units is encouraged (i.e. s instead of ms. Hz instead of kHz) to make better use of unit literals (see @ref Units)
81 /// @warning This method is only called to set a fixed value before processing, and is not called to do automation (via @ref hart::Envelope)
82 /// If you want your class to support automation for a specific parameter, override @ref supportsEnvelopeFor(), and then
83 /// use @c envelopeBuffers provided in @ref process() callback.
84 virtual void setValue (int paramId, double value) = 0;
85
86 /// @brief Retrieves DSP value
87 /// @details Among other things, it can be used to retrieve various readings like Gain Reduction measurements from your effect for further inspection
88 /// @param paramId Some ID that your subclass understands
89 /// @return The value of requested parameter in a unit that your subclass understands
90 /// @note This method is only intended for parameters that don't have an automation envelope attached to this specific instance.
91 /// To get values for automated parameters, use @c envelopeBuffers provided in @ref process() callback.
92 virtual double getValue (int /* paramId */) const
93 {
94 return 0.0;
95 }
96
97 /// @brief Tells the runner (host) whether this effect supports a specific i/o configuration.
98 /// @details It is guaranteed that the effect will not receive unsupported number of channels in @ref process().
99 /// However, it is not always possible to handle gracefully channel layout being unsupported, so in some circumstances
100 /// it can cause an exception or a test failure. This method is guaranteed to be called at least once before @ref prepare()
101 virtual bool supportsChannelLayout (size_t numInputChannels, size_t numOutputChannels) const = 0;
102
103 /// @brief Makes a text representation of this DSP effect for test failure outputs.
104 /// @details It is strongly encouraged to follow python's
105 /// <a href="https://docs.python.org/3/reference/datamodel.html#object.__repr__" target="_blank">repr()</a>
106 /// conventions for returned text - basically, put something like "MyClass(value1, value2)" (with no quotes)
107 /// into the stream whenever possible, or "<Readable info in angled brackets>" otherwise.
108 /// Also, use built-in stream manipulators like @ref dbPrecision wherever applicable.
109 /// Use @ref HART_DEFINE_GENERIC_REPRESENT() to get a basic implementation for this method.
110 /// @param[out] stream Output stream to write to
111 virtual void represent (std::ostream& stream) const = 0;
112
113 /// @brief Tells whether this effect accepts automation envelopes for a particular parameter
114 /// @param paramId Some ID that your subclass understands
115 /// @return true if your subclass can process automation for this parameter, false otherwise
116 virtual bool supportsEnvelopeFor (int /* paramId */) const { return false; }
117
118 /// @brief Tells whether this effect supports given sample rate
119 /// @details It is guaranteed to be called before @ref prepare()
120 /// @param sampleRateHz Sample rate in question
121 /// @return true if effect is capable of interpreting and processing in a given sample rate, false otherwise
122 virtual bool supportsSampleRate (double /* sampleRateHz */) const { return true; }
123
124 /// @brief Returns a smart pointer with a copy of this object
125 virtual std::unique_ptr<DSPBase<SampleType>> copy() const { return nullptr; }
126
127 /// @brief Returns a smart pointer with a moved instance of this object
128 virtual std::unique_ptr<DSPBase<SampleType>> move() = 0;
129
130 /// @brief Destructor
131 virtual ~DSPBase() = default;
132
133 /// @brief Default constructor
134 DSPBase() = default;
135
136 /// @brief Copies from another DSP effect instance
137 /// @details Attached automation envelopes are deep-copied
138 DSPBase (const DSPBase& other):
139 m_channelsToProcess (other.m_channelsToProcess)
140 {
141 for (auto& pair : other.m_envelopes)
142 m_envelopes.emplace (pair.first, pair.second->copy());
143 }
144
145 /// @brief Move constructor
146 /// @details Attached automation envelopes are moved as well
147 DSPBase (DSPBase&& other) noexcept:
148 m_envelopes (std::move (other.m_envelopes)),
149 m_channelsToProcess (other.m_channelsToProcess)
150 {
151 }
152
153 /// @brief Copies from another DSP effect instance
154 /// @details Attached automation envelopes are deep-copied
155 DSPBase& operator= (const DSPBase& other)
156 {
157 if (this == &other)
158 return *this;
159
160 for (auto& pair : other.m_envelopes)
161 m_envelopes.emplace (pair.first, pair.second->copy());
162
163 m_channelsToProcess = other.m_channelsToProcess;
164 return *this;
165 }
166
167 /// @brief Move assignment
168 /// @details Attached automation envelopes are moved as well
169 DSPBase& operator= (DSPBase&& other) noexcept
170 {
171 if (this != &other)
172 m_envelopes = std::move (other.m_envelopes);
173
174 m_channelsToProcess = other.m_channelsToProcess;
175 return *this;
176 }
177
178 /// @brief Checks if there's an automation envelope attached to a specific parameter
179 /// @details The envelopes are guaranteed to be attached strictly before @ref prepare()
180 /// callback, so by the time of the first @ref process() call consider the presence or
181 /// absence of envelope permanent.
182 /// @return Reference to itself for chaining
183 bool hasEnvelopeFor (int paramId)
184 {
185 return m_envelopes.find (paramId) != m_envelopes.end();
186 }
187
188 /// @brief Prepares all the attached envelopes and the effect itself for processing
189 /// @details This method is intended to be called by DSP hosts like @ref hart::AudioTestBuilder or @ref hart::Signal.
190 /// If you're making something that owns an instance of a DSP and needs it to generate audio,
191 /// you must call this method before calling @ref processWithEnvelopes().
192 /// You must also call @ref supportsChannelLayout() and @ref supportsSampleRate() before calling this method.
193 /// @attention If you're not making a custom host, you probably don't need to call this method.
194 void prepareWithEnvelopes (double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames)
195 {
196 m_envelopeBuffers.clear(); // TODO: Remove only unused buffers
197
198 for (auto& item : m_envelopes)
199 {
200 const int paramId = item.first;
201 m_envelopeBuffers.emplace (paramId, std::vector<double> (maxBlockSizeFrames));
202 }
203
204 hassert (m_envelopes.size() == m_envelopeBuffers.size());
205
206 for (auto& item : m_envelopeBuffers)
207 {
208 const int paramId = item.first;
209 auto& envelopeBuffer = item.second;
210
211 // Sanity checks
212 hassert (supportsEnvelopeFor (paramId) && "Envelope for this id is unsupported, yet there's an envelope buffer allocated for it");
213 hassert (hasEnvelopeFor (paramId) && "Envelope for this param id is not attached, yet there's an envelope buffer allocated for it");
214
215 if (envelopeBuffer.size() != maxBlockSizeFrames)
216 envelopeBuffer.resize (maxBlockSizeFrames);
217 }
218
219 m_channelsToProcess.resize (numInputChannels);
220 prepare (sampleRateHz, numInputChannels, numOutputChannels, maxBlockSizeFrames);
221 }
222
223 /// @brief Resets the DSP instance, and all associated Envelopes
224 /// @attention If you're not making a custom host, you probably don't need to call this method.
226 {
227 for (auto& item : m_envelopes)
228 {
229 std::unique_ptr<Envelope>& envelope = item.second;
230 envelope->reset();
231 }
232
233 reset();
234 }
235
236 /// @brief Returns a structure indicating which channels should be processed by this DSP
237 /// @details See `hart::DSP::atChannels()`, `hart::DSP::atChannel()`, `hart::DSP::atAllChannels()`
238 /// @return Set of flags for each channel, see `hart::ChannelFlags`.
239 /// `true` for channels that need processing, `false` for channels that need bypassing.
241 {
242 return m_channelsToProcess;
243 }
244
245 /// @brief Renders all the automation envelopes and processes the audio
246 /// @details This method is intended to be called by DSP hosts like @ref hart::AudioTestBuilder @ref hart::Signal.
247 /// If you're making something that owns an instance of a Signal and needs it to generate audio,
248 /// you must call it after calling @ref prepareWithEnvelopes().
249 /// @attention If you're not making a custom host, you probably don't need to call this method.
250 /// @param input Input audio block
251 /// @param output Output audio block
252 void processWithEnvelopes (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output)
253 {
254 for (auto& item : m_envelopeBuffers)
255 {
256 const int paramId = item.first;
257 auto& envelopeBuffer = item.second;
258
259 // Sanity checks
260 hassert (supportsEnvelopeFor (paramId) && "Envelope for this id is unsupported, yet there's an envelope buffer allocated for it");
261 hassert (hasEnvelopeFor (paramId) && "Envelope for this param id is not attached, yet there's an envelope buffer allocated for it");
262 hassert (input.getNumFrames() <= envelopeBuffer.size() && "Envelope Buffers were not allocated properly for this buffer size");
263
264 // Render envelope values
265 getValues (paramId, input.getNumFrames(), envelopeBuffer);
266 }
267
268 process (input, output, m_envelopeBuffers, m_channelsToProcess);
269 }
270
271 /// @brief Makes a text representation of this DSP with optional "atChannels" appendix
272 /// @details For internal use by hosts
273 void representWithActiveChannels (std::ostream& stream) const
274 {
275 this->represent (stream);
276
278 return;
279
280 stream << ".atChannels (";
282 stream << ')';
283 }
284
285 /// @brief Helper for template resolution
286 /// @private
287 using SampleTypePublicAlias = SampleType;
288
289protected:
290 std::unordered_map<int, std::unique_ptr<Envelope>> m_envelopes;
292
293private:
294 EnvelopeBuffers m_envelopeBuffers;
295
296 /// @brief Gets sample-accurate automation envelope values for a specific parameter
297 /// @param[in] paramId Some ID that your subclass understands
298 /// @param[in] blockSize Buffer size in frames, should be the same as ```input```/```output```'s size in @ref process()
299 /// @param[out] valuesOutput Container to get filled with the rendered automation values
300 void getValues (int paramId, size_t blockSize, std::vector<double>& valuesOutput)
301 {
302 if (valuesOutput.size() < blockSize)
303 {
304 HART_WARNING ("Make sure to configure your envelope container size before processing audio");
305 valuesOutput.resize (blockSize);
306 }
307
308 if (! hasEnvelopeFor (paramId))
309 {
310 const double value = getValue (paramId);
311 std::fill (valuesOutput.begin(), valuesOutput.end(), value);
312 }
313 else
314 {
315 m_envelopes[paramId]->renderNextBlock (blockSize, valuesOutput);
316 }
317 }
318};
319
320/// @brief Base for DSP effects
321/// @details This class is used both for adapting your DSP classes that you wish to test,
322/// and for using in DSP chains of @ref Signals, so you can use stock effects like @ref GainDb
323/// as tested processors, and you can use your tested DSP subclasses in Signals' DSP chains with
324/// other effects. You can even chain multiple of your own DSP classes together this way.
325/// All the callbacks of this class are guaranteed to be called from the same thread.
326/// To make your custom DSP wrapper, inherit from it like so
327/// (note the <a href="https://en.cppreference.com/w/cpp/language/crtp.html">CRTP</a>
328/// in the template args here, it's important!):
329/// @code{.cpp}
330/// template<typename SampleType>
331/// class MyCustomDSP: public DSP<SampleType, MyCustomDSP<SampleType>>
332/// {
333/// // ...
334/// };
335/// @endcode
336/// @tparam SampleType Type of values that will be processed, typically ```float``` or ```double```
337/// @tparam Derived Subclass for CRTP
338/// @ingroup DSP
339template<typename SampleType, typename Derived>
340class DSP:
341 public DSPBase<SampleType>
342{
343public:
344 /// @brief Adds envelope to a specific parameter by moving it
345 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
346 /// and only if it has returned ```true``` for this specific ```paramId```.
347 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
348 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
349 /// @param paramId Some ID that your subclass understands
350 /// @param envelope Envelope to be attached
351 /// @return Reference to itself for chaining
352 Derived& withEnvelope (int paramId, Envelope&& envelope) &
353 {
354 _withEnvelope (paramId, envelope);
355 return static_cast<Derived&> (*this);
356 }
357
358 /// @brief Adds envelope to a specific parameter by moving it
359 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
360 /// and only if it has returned ```true``` for this specific ```paramId```.
361 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
362 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
363 /// @param paramId Some ID that your subclass understands
364 /// @param envelope Envelope to be attached
365 /// @return Reference to itself for chaining
366 Derived&& withEnvelope (int paramId, Envelope&& envelope) &&
367 {
368 _withEnvelope (paramId, envelope);
369 return static_cast<Derived&&> (*this);
370 }
371
372 /// @brief Adds envelope to a specific parameter by copying it
373 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
374 /// and only if it has returned ```true``` for this specific ```paramId```.
375 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
376 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
377 /// @param paramId Some ID that your subclass understands
378 /// @param envelope Envelope to be attached
379 /// @return Reference to itself for chaining
380 Derived& withEnvelope (int paramId, const Envelope& envelope) &
381 {
382 _withEnvelope (paramId, envelope);
383 return static_cast<Derived&> (*this);
384 }
385
386 /// @brief Adds envelope to a specific parameter by copying it
387 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
388 /// and only if it has returned ```true``` for this specific ```paramId```.
389 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
390 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
391 /// @param paramId Some ID that your subclass understands
392 /// @param envelope Envelope to be attached
393 /// @return Reference to itself for chaining
394 Derived&& withEnvelope (int paramId, const Envelope& envelope) &&
395 {
396 _withEnvelope (paramId, envelope);
397 return static_cast<Derived&&> (*this);
398 }
399
400 // TODO: withEnvelope() that takes a unique_ptr to the envelope
401
402 /// @brief Returns a smart pointer with a moved instance of this object
403 virtual std::unique_ptr<DSPBase<SampleType>> move() override
404 {
405 return hart::make_unique<Derived> (std::move (static_cast<Derived&> (*this)));
406 }
407
408 /// @brief Makes this DSP process only specific channels, and ignore the rest
409 /// @details If not set, the DSP applies to all channels by default.
410 /// If you call this method multiple times, only the last one will be applied.
411 /// To select only one channel, consider using @ref DSP::atChannel() instead.
412 /// @param channelsToProcess List of channels this DSP should apply to,
413 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` for left and right channels only.
414 /// @see `hart::Channel`
415 Derived& atChannels (std::initializer_list<size_t> channelsToProcess) &
416 {
417 _atChannels (channelsToProcess);
418 return static_cast<Derived&> (*this);
419 }
420
421 /// @brief Makes this DSP process only specific channels, and ignore the rest
422 /// @details If not set, the DSP applies to all channels by default.
423 /// If you call this method multiple times, only the last one will be applied.
424 /// To select only one channel, consider using @ref DSP::atChannel() instead.
425 /// @param channelsToProcess List of channels this DSP should apply to,
426 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` for left and right channels only.
427 /// @see `hart::Channel`
428 Derived&& atChannels (std::initializer_list<size_t> channelsToProcess) &&
429 {
430 _atChannels (channelsToProcess);
431 return static_cast<Derived&&> (*this);
432 }
433
434 /// @brief Makes this DSP process only specific channels, and bypass the rest
435 /// @details If not set, the DSP applies to all channels by default.
436 /// If you call this method multiple times, only the last one will be applied.
437 /// To select multiple channels, use @ref DSP::atChannels() or
438 /// @ref atAllChannelsExcept() instead.
439 /// @param channelToProcess Channel this DSP should apply to (zero-based),
440 /// e.g. `0` or `Channel::left` for left channel.
441 /// @note If not set, the DSP applies to all channels by default
442 /// @see `hart::Channel`
443 Derived& atChannel (size_t channelToProcess) &
444 {
445 _atChannel (channelToProcess);
446 return static_cast<Derived&> (*this);
447 }
448
449 /// @brief Makes this DSP process only specific channels, and bypass the rest
450 /// @details If not set, the DSP applies to all channels by default.
451 /// If you call this method multiple times, only the last one will be applied.
452 /// To select multiple channels, use @ref DSP::atChannels() or
453 /// @ref atAllChannelsExcept() instead.
454 /// @param channelToProcess Channel this DSP should apply to (zero-based),
455 /// e.g. `0` or `Channel::left` for left channel.
456 /// @note If not set, the DSP applies to all channels by default
457 /// @see `hart::Channel`
458 Derived&& atChannel (size_t channelToProcess) &&
459 {
460 _atChannel (channelToProcess);
461 return static_cast<Derived&&> (*this);
462 }
463
464 /// @brief Makes this DSP apply to all channels
465 /// @details This is the default setting anyway, so this method is only
466 /// for cases when you need to override previous @ref atChannel(),
467 /// @ref atChannels() or @ref atAllChannelsExcept() calls.
468 Derived& atAllChannels() &
469 {
470 this->m_channelsToProcess.setAllTo (true);
471 return static_cast<Derived&> (*this);
472 }
473
474 /// @brief Makes this DSP apply to all channels
475 /// @details This is the default setting anyway, so this method is only
476 /// for cases when you need to override previous @ref atChannel(),
477 /// @ref atChannels() or @ref atAllChannelsExcept() calls.
478 Derived&& atAllChannels() &&
479 {
480 this->m_channelsToProcess.setAllTo (true);
481 return static_cast<Derived&&> (*this);
482 }
483
484 /// @brief Makes this DSP process only specific channels, and bypass the rest
485 /// @details If not set, DSP applies to all channels by default.
486 /// If you call this method multiple times, only the last one will be applied.
487 /// @param channelsToSkip List of channels this DSP should NOT apply to,
488 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` to bypass left and right
489 /// channels, and process the rest
490 /// @see `hart::Channel`
491 Derived& atAllChannelsExcept (std::initializer_list<size_t> channelsToSkip) &
492 {
493 _atAllChannelsExcept (channelsToSkip);
494 return static_cast<Derived&> (*this);
495 }
496
497 /// @brief Makes this DSP process only specific channels, and bypass the rest
498 /// @details If not set, DSP applies to all channels by default.
499 /// If you call this method multiple times, only the last one will be applied.
500 /// @param channelsToSkip List of channels this DSP should NOT apply to,
501 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` to bypass left and right
502 /// channels, and process the rest
503 /// @see `hart::Channel`
504 Derived&& atAllChannelsExcept (std::initializer_list<size_t> channelsToSkip) &&
505 {
506 _atAllChannelsExcept (channelsToSkip);
507 return static_cast<Derived&&> (*this);
508 }
509
510private:
511 void _withEnvelope (int paramId, Envelope&& envelope)
512 {
513 if (! this->supportsEnvelopeFor (paramId))
514 HART_THROW_OR_RETURN (hart::UnsupportedError, std::string ("DSP doesn't support envelopes for param ID: ") + std::to_string (paramId), *this);
515
516 this->m_envelopes.emplace (paramId, hart::make_unique<Envelope> (std::move (envelope)));
517 }
518
519 void _withEnvelope (int paramId, const Envelope& envelope)
520 {
521 if (! this->supportsEnvelopeFor(paramId))
522 HART_THROW_OR_RETURN (hart::UnsupportedError, std::string ("DSP doesn't support envelopes for param ID: ") + std::to_string (paramId), *this);
523
524 this->m_envelopes.emplace (paramId, envelope.copy());
525 }
526
527 void _atChannels (std::initializer_list<size_t> channelsToProcess)
528 {
529 this->m_channelsToProcess.setAllTo (false);
530
531 for (size_t channel : channelsToProcess)
532 {
533 if (channel >= this->m_channelsToProcess.size())
534 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Channel exceeds max number of channels");
535
536 this->m_channelsToProcess[channel] = true;
537 }
538 }
539
540 void _atChannel (size_t channelToProcess)
541 {
542 if (channelToProcess >= this->m_channelsToProcess.size())
543 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Channel exceeds max number of channels");
544
545 this->m_channelsToProcess.setAllTo (false);
546 this->m_channelsToProcess[channelToProcess] = true;
547 }
548
549 void _atAllChannelsExcept (std::initializer_list<size_t> channelsToSkip)
550 {
551 this->m_channelsToProcess.setAllTo (true);
552
553 for (size_t channel : channelsToSkip)
554 {
555 if (channel >= this->m_channelsToProcess.size())
556 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Channel exceeds max number of channels");
557
558 this->m_channelsToProcess[channel] = false;
559 }
560 }
561};
562
563/// @brief Prints readable text representation of the DSP object into the I/O stream
564/// @relates DSP
565/// @ingroup DSP
566template <typename SampleType>
567inline std::ostream& operator<< (std::ostream& stream, const DSPBase<SampleType>& dsp)
568{
569 // TODO: Represent with the envelopes
570 dsp.representWithActiveChannels (stream);
571 return stream;
572}
573
574/// @brief Forbids @ref hart::DSP::copy() method
575/// @details Put this into your class body's ```public``` section if either is true:
576/// - Your class is not trivially copyable
577/// - You don't want to trouble yourself with implementing copy semantics for your class
578/// @ingroup DSP
579#define HART_DSP_NON_COPYABLE
580 std::unique_ptr<DSP<SampleType>> copy() const override { return nullptr; }
581
582/// @brief Forbids @ref hart::DSP::move() method
583/// @details Put this into your class body's ```public``` section if either is true:
584/// - Your class is not trivially movable
585/// - You don't want to trouble yourself with implementing move semantics for your class
586///
587/// Obviously, you won't be able to pass your class to the host
588/// by rvalue or explicit move, but you still can pass
589/// it wrapped into a smart pointer like so:
590/// ```cpp
591/// processAudioWith (hart::make_unique<MyDspType>()).withThis().withThat().process();
592/// ```
593/// @ingroup DSP
594#define HART_DSP_NON_MOVABLE
595 std::unique_ptr<DSP<SampleType>> move() override { return nullptr; }
596
597/// @brief Implements a generic @ref hart::DSP::copy() method
598/// @ingroup DSP
599#define HART_DSP_COPYABLE(ClassName) virtual
600 std::unique_ptr<DSPBase<SampleType>> copy() const override \
601{
602 return hart::make_unique<ClassName> (static_cast<const ClassName&> (*this)); \
603}
604
605/// @brief Implements a generic @ref hart::DSP::move() method
606/// @ingroup DSP
607#define HART_DSP_MOVABLE(ClassName) virtual
608 std::unique_ptr<DSPBase<SampleType>> move() override \
609{
610 return hart::make_unique<ClassName> (std::move (static_cast<ClassName&> (*this))); \
611}
612
613} // namespace hart
614
615/// @private
616#define HART_DSP_DECLARE_ALIASES_FOR(ClassName)
617 namespace aliases_float{
618 using ClassName = hart::ClassName<float>;
619 }
620 namespace aliases_double{
621 using ClassName = hart::ClassName<double>;
622 }
Container for audio data.
A set of boolean flags mapped to each audio channel.
bool allTrue() const noexcept
Checks if all flags are set to true
ChannelFlags(bool defaultValues=true, size_t numChannels=m_maxChannels)
Creates a new channel flags object.
void resize(size_t newNumChannels)
Resizes the container.
void representAsInitializerList(std::ostream &stream) const
Makes text representation of itself as a initializer list of active channels.
Polymorphic base for all DSP.
Definition hart_dsp.hpp:33
virtual ~DSPBase()=default
Destructor.
DSPBase(const DSPBase &other)
Copies from another DSP effect instance.
Definition hart_dsp.hpp:138
virtual bool supportsChannelLayout(size_t numInputChannels, size_t numOutputChannels) const =0
Tells the runner (host) whether this effect supports a specific i/o configuration.
void prepareWithEnvelopes(double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames)
Prepares all the attached envelopes and the effect itself for processing.
Definition hart_dsp.hpp:194
void representWithActiveChannels(std::ostream &stream) const
Makes a text representation of this DSP with optional "atChannels" appendix.
Definition hart_dsp.hpp:273
virtual std::unique_ptr< DSPBase< SampleType > > move()=0
Returns a smart pointer with a moved instance of this object.
virtual std::unique_ptr< DSPBase< SampleType > > copy() const
Returns a smart pointer with a copy of this object.
Definition hart_dsp.hpp:125
virtual void represent(std::ostream &stream) const =0
Makes a text representation of this DSP effect for test failure outputs.
ChannelFlags m_channelsToProcess
Definition hart_dsp.hpp:291
virtual void reset()
Resets to initial state.
Definition hart_dsp.hpp:74
DSPBase(DSPBase &&other) noexcept
Move constructor.
Definition hart_dsp.hpp:147
virtual bool supportsSampleRate(double) const
Tells whether this effect supports given sample rate.
Definition hart_dsp.hpp:122
void processWithEnvelopes(const AudioBuffer< SampleType > &input, AudioBuffer< SampleType > &output)
Renders all the automation envelopes and processes the audio.
Definition hart_dsp.hpp:252
DSPBase()=default
Default constructor.
virtual double getValue(int) const
Retrieves DSP value.
Definition hart_dsp.hpp:92
virtual void prepare(double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames)=0
Prepare for processing.
virtual void setValue(int paramId, double value)=0
Sets DSP value.
void resetWithEnvelopes()
Resets the DSP instance, and all associated Envelopes.
Definition hart_dsp.hpp:225
DSPBase & operator=(const DSPBase &other)
Copies from another DSP effect instance.
Definition hart_dsp.hpp:155
bool hasEnvelopeFor(int paramId)
Checks if there's an automation envelope attached to a specific parameter.
Definition hart_dsp.hpp:183
virtual void process(const AudioBuffer< SampleType > &input, AudioBuffer< SampleType > &output, const EnvelopeBuffers &envelopeBuffers, ChannelFlags channelsToProcess)=0
Processes the audio.
ChannelFlags getChannelsToProcess()
Returns a structure indicating which channels should be processed by this DSP.
Definition hart_dsp.hpp:240
DSPBase & operator=(DSPBase &&other) noexcept
Move assignment.
Definition hart_dsp.hpp:169
virtual bool supportsEnvelopeFor(int) const
Tells whether this effect accepts automation envelopes for a particular parameter.
Definition hart_dsp.hpp:116
std::unordered_map< int, std::unique_ptr< Envelope > > m_envelopes
Definition hart_dsp.hpp:290
Base for DSP effects.
Definition hart_dsp.hpp:342
virtual std::unique_ptr< DSPBase< SampleType > > move() override
Returns a smart pointer with a moved instance of this object.
Definition hart_dsp.hpp:403
Derived && atChannel(size_t channelToProcess) &&
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:458
Derived && withEnvelope(int paramId, const Envelope &envelope) &&
Adds envelope to a specific parameter by copying it.
Definition hart_dsp.hpp:394
Derived && withEnvelope(int paramId, Envelope &&envelope) &&
Adds envelope to a specific parameter by moving it.
Definition hart_dsp.hpp:366
Derived & atAllChannelsExcept(std::initializer_list< size_t > channelsToSkip) &
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:491
Derived & withEnvelope(int paramId, const Envelope &envelope) &
Adds envelope to a specific parameter by copying it.
Definition hart_dsp.hpp:380
Derived & atAllChannels() &
Makes this DSP apply to all channels.
Definition hart_dsp.hpp:468
Derived && atAllChannels() &&
Makes this DSP apply to all channels.
Definition hart_dsp.hpp:478
Derived && atChannels(std::initializer_list< size_t > channelsToProcess) &&
Makes this DSP process only specific channels, and ignore the rest.
Definition hart_dsp.hpp:428
Derived & atChannels(std::initializer_list< size_t > channelsToProcess) &
Makes this DSP process only specific channels, and ignore the rest.
Definition hart_dsp.hpp:415
Derived & withEnvelope(int paramId, Envelope &&envelope) &
Adds envelope to a specific parameter by moving it.
Definition hart_dsp.hpp:352
Derived & atChannel(size_t channelToProcess) &
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:443
Derived && atAllChannelsExcept(std::initializer_list< size_t > channelsToSkip) &&
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:504
Represents an Envelope curve for DSP parameters.
virtual void reset()=0
virtual void renderNextBlock(size_t blockSize, std::vector< double > &valuesOutput)=0
virtual std::unique_ptr< Envelope > copy() const =0
Thrown when some parameter has an unsupported value.
Thrown when an inappropriate value is encountered.
#define HART_THROW_OR_RETURN_VOID(ExceptionType, message)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns otherwise.
#define HART_WARNING(message)
Prints a warning message.
#define hassert(condition)
Triggers a HartAssertException if the condition is false
#define HART_THROW_OR_RETURN(ExceptionType, message, returnValue)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns a specified ...
std::unique_ptr< ObjectType > make_unique(Args &&... args)
std::make_unique() replacement for C++11