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 std::unique_ptr<Envelope>& envelope = item.second;
204 envelope->prepare (sampleRateHz, maxBlockSizeFrames);
205 }
206
207 hassert (m_envelopes.size() == m_envelopeBuffers.size());
208
209 for (auto& item : m_envelopeBuffers)
210 {
211 const int paramId = item.first;
212 auto& envelopeBuffer = item.second;
213
214 // Sanity checks
215 hassert (supportsEnvelopeFor (paramId) && "Envelope for this id is unsupported, yet there's an envelope buffer allocated for it");
216 hassert (hasEnvelopeFor (paramId) && "Envelope for this param id is not attached, yet there's an envelope buffer allocated for it");
217
218 if (envelopeBuffer.size() != maxBlockSizeFrames)
219 envelopeBuffer.resize (maxBlockSizeFrames);
220 }
221
222 m_channelsToProcess.resize (numInputChannels);
223 prepare (sampleRateHz, numInputChannels, numOutputChannels, maxBlockSizeFrames);
224 }
225
226 /// @brief Resets the DSP instance, and all associated Envelopes
227 /// @attention If you're not making a custom host, you probably don't need to call this method.
229 {
230 for (auto& item : m_envelopes)
231 {
232 std::unique_ptr<Envelope>& envelope = item.second;
233 envelope->reset();
234 }
235
236 reset();
237 }
238
239 /// @brief Returns a structure indicating which channels should be processed by this DSP
240 /// @details See `hart::DSP::atChannels()`, `hart::DSP::atChannel()`, `hart::DSP::atAllChannels()`
241 /// @return Set of flags for each channel, see `hart::ChannelFlags`.
242 /// `true` for channels that need processing, `false` for channels that need bypassing.
244 {
245 return m_channelsToProcess;
246 }
247
248 /// @brief Renders all the automation envelopes and processes the audio
249 /// @details This method is intended to be called by DSP hosts like @ref hart::AudioTestBuilder @ref hart::Signal.
250 /// If you're making something that owns an instance of a Signal and needs it to generate audio,
251 /// you must call it after calling @ref prepareWithEnvelopes().
252 /// @attention If you're not making a custom host, you probably don't need to call this method.
253 /// @param input Input audio block
254 /// @param output Output audio block
255 void processWithEnvelopes (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output)
256 {
257 for (auto& item : m_envelopeBuffers)
258 {
259 const int paramId = item.first;
260 auto& envelopeBuffer = item.second;
261
262 // Sanity checks
263 hassert (supportsEnvelopeFor (paramId) && "Envelope for this id is unsupported, yet there's an envelope buffer allocated for it");
264 hassert (hasEnvelopeFor (paramId) && "Envelope for this param id is not attached, yet there's an envelope buffer allocated for it");
265 hassert (input.getNumFrames() <= envelopeBuffer.size() && "Envelope Buffers were not allocated properly for this buffer size");
266
267 // Render envelope values
268 getValues (paramId, input.getNumFrames(), envelopeBuffer);
269 }
270
271 process (input, output, m_envelopeBuffers, m_channelsToProcess);
272 }
273
274 /// @brief Makes a text representation of this DSP with optional "atChannels" appendix
275 /// @details For internal use by hosts
276 void representWithActiveChannels (std::ostream& stream) const
277 {
278 this->represent (stream);
279
281 return;
282
283 stream << ".atChannels (";
285 stream << ')';
286 }
287
288 /// @brief Helper for template resolution
289 /// @private
290 using SampleTypePublicAlias = SampleType;
291
292protected:
293 std::unordered_map<int, std::unique_ptr<Envelope>> m_envelopes;
295
296private:
297 EnvelopeBuffers m_envelopeBuffers;
298
299 /// @brief Gets sample-accurate automation envelope values for a specific parameter
300 /// @param[in] paramId Some ID that your subclass understands
301 /// @param[in] blockSize Buffer size in frames, should be the same as ```input```/```output```'s size in @ref process()
302 /// @param[out] valuesOutput Container to get filled with the rendered automation values
303 void getValues (int paramId, size_t blockSize, std::vector<double>& valuesOutput)
304 {
305 if (valuesOutput.size() < blockSize)
306 {
307 HART_WARNING ("Make sure to configure your envelope container size before processing audio");
308 valuesOutput.resize (blockSize);
309 }
310
311 if (! hasEnvelopeFor (paramId))
312 {
313 const double value = getValue (paramId);
314 std::fill (valuesOutput.begin(), valuesOutput.end(), value);
315 }
316 else
317 {
318 m_envelopes[paramId]->renderNextBlock (blockSize, valuesOutput);
319 }
320 }
321};
322
323/// @brief Base for DSP effects
324/// @details This class is used both for adapting your DSP classes that you wish to test,
325/// and for using in DSP chains of @ref Signals, so you can use stock effects like @ref GainDb
326/// as tested processors, and you can use your tested DSP subclasses in Signals' DSP chains with
327/// other effects. You can even chain multiple of your own DSP classes together this way.
328/// All the callbacks of this class are guaranteed to be called from the same thread.
329/// To make your custom DSP wrapper, inherit from it like so
330/// (note the <a href="https://en.cppreference.com/w/cpp/language/crtp.html">CRTP</a>
331/// in the template args here, it's important!):
332/// @code{.cpp}
333/// template<typename SampleType>
334/// class MyCustomDSP: public DSP<SampleType, MyCustomDSP<SampleType>>
335/// {
336/// // ...
337/// };
338/// @endcode
339/// @tparam SampleType Type of values that will be processed, typically ```float``` or ```double```
340/// @tparam Derived Subclass for CRTP
341/// @ingroup DSP
342template<typename SampleType, typename Derived>
343class DSP:
344 public DSPBase<SampleType>
345{
346public:
347 /// @brief Adds envelope to a specific parameter by moving it
348 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
349 /// and only if it has returned ```true``` for this specific ```paramId```.
350 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
351 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
352 /// @param paramId Some ID that your subclass understands
353 /// @param envelope Envelope to be attached
354 /// @return Reference to itself for chaining
355 Derived& withEnvelope (int paramId, Envelope&& envelope) &
356 {
357 _withEnvelope (paramId, envelope);
358 return static_cast<Derived&> (*this);
359 }
360
361 /// @brief Adds envelope to a specific parameter by moving it
362 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
363 /// and only if it has returned ```true``` for this specific ```paramId```.
364 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
365 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
366 /// @param paramId Some ID that your subclass understands
367 /// @param envelope Envelope to be attached
368 /// @return Reference to itself for chaining
369 Derived&& withEnvelope (int paramId, Envelope&& envelope) &&
370 {
371 _withEnvelope (paramId, envelope);
372 return static_cast<Derived&&> (*this);
373 }
374
375 /// @brief Adds envelope to a specific parameter by copying it
376 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
377 /// and only if it has returned ```true``` for this specific ```paramId```.
378 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
379 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
380 /// @param paramId Some ID that your subclass understands
381 /// @param envelope Envelope to be attached
382 /// @return Reference to itself for chaining
383 Derived& withEnvelope (int paramId, const Envelope& envelope) &
384 {
385 _withEnvelope (paramId, envelope);
386 return static_cast<Derived&> (*this);
387 }
388
389 /// @brief Adds envelope to a specific parameter by copying it
390 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
391 /// and only if it has returned ```true``` for this specific ```paramId```.
392 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
393 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
394 /// @param paramId Some ID that your subclass understands
395 /// @param envelope Envelope to be attached
396 /// @return Reference to itself for chaining
397 Derived&& withEnvelope (int paramId, const Envelope& envelope) &&
398 {
399 _withEnvelope (paramId, envelope);
400 return static_cast<Derived&&> (*this);
401 }
402
403 // TODO: withEnvelope() that takes a unique_ptr to the envelope
404
405 /// @brief Returns a smart pointer with a moved instance of this object
406 virtual std::unique_ptr<DSPBase<SampleType>> move() override
407 {
408 return hart::make_unique<Derived> (std::move (static_cast<Derived&> (*this)));
409 }
410
411 /// @brief Makes this DSP process only specific channels, and ignore the rest
412 /// @details If not set, the DSP applies to all channels by default.
413 /// If you call this method multiple times, only the last one will be applied.
414 /// To select only one channel, consider using @ref DSP::atChannel() instead.
415 /// @param channelsToProcess List of channels this DSP should apply to,
416 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` for left and right channels only.
417 /// @see `hart::Channel`
418 Derived& atChannels (std::initializer_list<size_t> channelsToProcess) &
419 {
420 _atChannels (channelsToProcess);
421 return static_cast<Derived&> (*this);
422 }
423
424 /// @brief Makes this DSP process only specific channels, and ignore the rest
425 /// @details If not set, the DSP applies to all channels by default.
426 /// If you call this method multiple times, only the last one will be applied.
427 /// To select only one channel, consider using @ref DSP::atChannel() instead.
428 /// @param channelsToProcess List of channels this DSP should apply to,
429 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` for left and right channels only.
430 /// @see `hart::Channel`
431 Derived&& atChannels (std::initializer_list<size_t> channelsToProcess) &&
432 {
433 _atChannels (channelsToProcess);
434 return static_cast<Derived&&> (*this);
435 }
436
437 /// @brief Makes this DSP process only specific channels, and bypass the rest
438 /// @details If not set, the DSP applies to all channels by default.
439 /// If you call this method multiple times, only the last one will be applied.
440 /// To select multiple channels, use @ref DSP::atChannels() or
441 /// @ref atAllChannelsExcept() instead.
442 /// @param channelToProcess Channel this DSP should apply to (zero-based),
443 /// e.g. `0` or `Channel::left` for left channel.
444 /// @note If not set, the DSP applies to all channels by default
445 /// @see `hart::Channel`
446 Derived& atChannel (size_t channelToProcess) &
447 {
448 _atChannel (channelToProcess);
449 return static_cast<Derived&> (*this);
450 }
451
452 /// @brief Makes this DSP process only specific channels, and bypass the rest
453 /// @details If not set, the DSP applies to all channels by default.
454 /// If you call this method multiple times, only the last one will be applied.
455 /// To select multiple channels, use @ref DSP::atChannels() or
456 /// @ref atAllChannelsExcept() instead.
457 /// @param channelToProcess Channel this DSP should apply to (zero-based),
458 /// e.g. `0` or `Channel::left` for left channel.
459 /// @note If not set, the DSP applies to all channels by default
460 /// @see `hart::Channel`
461 Derived&& atChannel (size_t channelToProcess) &&
462 {
463 _atChannel (channelToProcess);
464 return static_cast<Derived&&> (*this);
465 }
466
467 /// @brief Makes this DSP apply to all channels
468 /// @details This is the default setting anyway, so this method is only
469 /// for cases when you need to override previous @ref atChannel(),
470 /// @ref atChannels() or @ref atAllChannelsExcept() calls.
471 Derived& atAllChannels() &
472 {
473 this->m_channelsToProcess.setAllTo (true);
474 return static_cast<Derived&> (*this);
475 }
476
477 /// @brief Makes this DSP apply to all channels
478 /// @details This is the default setting anyway, so this method is only
479 /// for cases when you need to override previous @ref atChannel(),
480 /// @ref atChannels() or @ref atAllChannelsExcept() calls.
481 Derived&& atAllChannels() &&
482 {
483 this->m_channelsToProcess.setAllTo (true);
484 return static_cast<Derived&&> (*this);
485 }
486
487 /// @brief Makes this DSP process only specific channels, and bypass the rest
488 /// @details If not set, DSP applies to all channels by default.
489 /// If you call this method multiple times, only the last one will be applied.
490 /// @param channelsToSkip List of channels this DSP should NOT apply to,
491 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` to bypass left and right
492 /// channels, and process the rest
493 /// @see `hart::Channel`
494 Derived& atAllChannelsExcept (std::initializer_list<size_t> channelsToSkip) &
495 {
496 _atAllChannelsExcept (channelsToSkip);
497 return static_cast<Derived&> (*this);
498 }
499
500 /// @brief Makes this DSP process only specific channels, and bypass the rest
501 /// @details If not set, DSP applies to all channels by default.
502 /// If you call this method multiple times, only the last one will be applied.
503 /// @param channelsToSkip List of channels this DSP should NOT apply to,
504 /// e.g. `{0, 1}` or `{Channel::left, Channel::right}` to bypass left and right
505 /// channels, and process the rest
506 /// @see `hart::Channel`
507 Derived&& atAllChannelsExcept (std::initializer_list<size_t> channelsToSkip) &&
508 {
509 _atAllChannelsExcept (channelsToSkip);
510 return static_cast<Derived&&> (*this);
511 }
512
513private:
514 void _withEnvelope (int paramId, Envelope&& envelope)
515 {
516 if (! this->supportsEnvelopeFor (paramId))
517 HART_THROW_OR_RETURN (hart::UnsupportedError, std::string ("DSP doesn't support envelopes for param ID: ") + std::to_string (paramId), *this);
518
519 this->m_envelopes.emplace (paramId, hart::make_unique<Envelope> (std::move (envelope)));
520 }
521
522 void _withEnvelope (int paramId, const Envelope& envelope)
523 {
524 if (! this->supportsEnvelopeFor(paramId))
525 HART_THROW_OR_RETURN (hart::UnsupportedError, std::string ("DSP doesn't support envelopes for param ID: ") + std::to_string (paramId), *this);
526
527 this->m_envelopes.emplace (paramId, envelope.copy());
528 }
529
530 void _atChannels (std::initializer_list<size_t> channelsToProcess)
531 {
532 this->m_channelsToProcess.setAllTo (false);
533
534 for (size_t channel : channelsToProcess)
535 {
536 if (channel >= this->m_channelsToProcess.size())
537 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Channel exceeds max number of channels");
538
539 this->m_channelsToProcess[channel] = true;
540 }
541 }
542
543 void _atChannel (size_t channelToProcess)
544 {
545 if (channelToProcess >= this->m_channelsToProcess.size())
546 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Channel exceeds max number of channels");
547
548 this->m_channelsToProcess.setAllTo (false);
549 this->m_channelsToProcess[channelToProcess] = true;
550 }
551
552 void _atAllChannelsExcept (std::initializer_list<size_t> channelsToSkip)
553 {
554 this->m_channelsToProcess.setAllTo (true);
555
556 for (size_t channel : channelsToSkip)
557 {
558 if (channel >= this->m_channelsToProcess.size())
559 HART_THROW_OR_RETURN_VOID (hart::ValueError, "Channel exceeds max number of channels");
560
561 this->m_channelsToProcess[channel] = false;
562 }
563 }
564};
565
566/// @brief Prints readable text representation of the DSP object into the I/O stream
567/// @relates DSP
568/// @ingroup DSP
569template <typename SampleType>
570inline std::ostream& operator<< (std::ostream& stream, const DSPBase<SampleType>& dsp)
571{
572 // TODO: Represent with the envelopes
573 dsp.representWithActiveChannels (stream);
574 return stream;
575}
576
577/// @brief Forbids @ref hart::DSP::copy() method
578/// @details Put this into your class body's ```public``` section if either is true:
579/// - Your class is not trivially copyable
580/// - You don't want to trouble yourself with implementing copy semantics for your class
581/// @ingroup DSP
582#define HART_DSP_NON_COPYABLE
583 std::unique_ptr<DSP<SampleType>> copy() const override { return nullptr; }
584
585/// @brief Forbids @ref hart::DSP::move() method
586/// @details Put this into your class body's ```public``` section if either is true:
587/// - Your class is not trivially movable
588/// - You don't want to trouble yourself with implementing move semantics for your class
589///
590/// Obviously, you won't be able to pass your class to the host
591/// by rvalue or explicit move, but you still can pass
592/// it wrapped into a smart pointer like so:
593/// ```cpp
594/// processAudioWith (hart::make_unique<MyDspType>()).withThis().withThat().process();
595/// ```
596/// @ingroup DSP
597#define HART_DSP_NON_MOVABLE
598 std::unique_ptr<DSP<SampleType>> move() override { return nullptr; }
599
600/// @brief Implements a generic @ref hart::DSP::copy() method
601/// @ingroup DSP
602#define HART_DSP_COPYABLE(ClassName) virtual
603 std::unique_ptr<DSPBase<SampleType>> copy() const override \
604{
605 return hart::make_unique<ClassName> (static_cast<const ClassName&> (*this)); \
606}
607
608/// @brief Implements a generic @ref hart::DSP::move() method
609/// @ingroup DSP
610#define HART_DSP_MOVABLE(ClassName) virtual
611 std::unique_ptr<DSPBase<SampleType>> move() override \
612{
613 return hart::make_unique<ClassName> (std::move (static_cast<ClassName&> (*this))); \
614}
615
616} // namespace hart
617
618/// @private
619#define HART_DSP_DECLARE_ALIASES_FOR(ClassName)
620 namespace aliases_float{
621 using ClassName = hart::ClassName<float>;
622 }
623 namespace aliases_double{
624 using ClassName = hart::ClassName<double>;
625 }
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:276
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:294
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:255
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:228
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:243
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:293
Base for DSP effects.
Definition hart_dsp.hpp:345
virtual std::unique_ptr< DSPBase< SampleType > > move() override
Returns a smart pointer with a moved instance of this object.
Definition hart_dsp.hpp:406
Derived && atChannel(size_t channelToProcess) &&
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:461
Derived && withEnvelope(int paramId, const Envelope &envelope) &&
Adds envelope to a specific parameter by copying it.
Definition hart_dsp.hpp:397
Derived && withEnvelope(int paramId, Envelope &&envelope) &&
Adds envelope to a specific parameter by moving it.
Definition hart_dsp.hpp:369
Derived & atAllChannelsExcept(std::initializer_list< size_t > channelsToSkip) &
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:494
Derived & withEnvelope(int paramId, const Envelope &envelope) &
Adds envelope to a specific parameter by copying it.
Definition hart_dsp.hpp:383
Derived & atAllChannels() &
Makes this DSP apply to all channels.
Definition hart_dsp.hpp:471
Derived && atAllChannels() &&
Makes this DSP apply to all channels.
Definition hart_dsp.hpp:481
Derived && atChannels(std::initializer_list< size_t > channelsToProcess) &&
Makes this DSP process only specific channels, and ignore the rest.
Definition hart_dsp.hpp:431
Derived & atChannels(std::initializer_list< size_t > channelsToProcess) &
Makes this DSP process only specific channels, and ignore the rest.
Definition hart_dsp.hpp:418
Derived & withEnvelope(int paramId, Envelope &&envelope) &
Adds envelope to a specific parameter by moving it.
Definition hart_dsp.hpp:355
Derived & atChannel(size_t channelToProcess) &
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:446
Derived && atAllChannelsExcept(std::initializer_list< size_t > channelsToSkip) &&
Makes this DSP process only specific channels, and bypass the rest.
Definition hart_dsp.hpp:507
Represents an Envelope curve for DSP parameters.
virtual void reset()=0
virtual void renderNextBlock(size_t blockSize, std::vector< double > &valuesOutput)=0
virtual void prepare(double sampleRateHz, size_t maxBlockSizeFrames)=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