HART  0.1.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
10#include "envelopes/hart_envelope.hpp"
11#include "hart_utils.hpp" // make_unique()
12
13namespace hart
14{
15
16/// @defgroup DSP DSP
17/// @brief Process signals
18
19/// @brief Hash table of automation envelope sequences mapped to param ids.
20/// @details Keys: Param IDs (int enums like GainDb::gainDb)
21/// Values: Sequence of automation envelope values for this Param ID, one value per frame
22using EnvelopeBuffers = std::unordered_map<int, std::vector<double>>;
23
24/// @brief Base for DSP effects
25/// @details This class is used both for adapting your DSP classes that you wish to test,
26/// and for using in DSP chains of @ref Signals, so you can use stock effects like @ref GainDb
27/// as tested processors, and you can use your tested DSP subclasses in Signals' DSP chains with
28/// other effects. You can even chain multiple of your own DSP classes together this way.
29/// All the callbacks of this class are guaranteed to be called from the same thread.
30/// @tparam SampleType Type of values that will be processed, typically ```float``` or ```double```
31/// @ingroup DSP
32template <typename SampleType>
33class DSP
34{
35public:
36 /// @brief Prepare for processing
37 /// @details In real-time DSP, such methods are usually used for allocating memory and other non-realtime-safe and heavyweight
38 /// operations. But keep in mind that that HART does not do real-time processing, so this merely follows common real-time
39 /// DSP design conventions, where non-realtime operations are done in a separate callback like this one.
40 /// This method is guaranteed to be called after @ref supportsChannelLayout() and @ref supportsSampleRate(), but before @ref process().
41 /// It is guaranteed that the number of input and output channels obeys supportsChannelLayout() and supportsSampleRate() preferences.
42 /// It is guaranteed that all subsequent process() calls will be in line with the arguments received in this callback.
43 /// @param sampleRateHz sample rate at which the audio should be interpreted and processed
44 /// @param numInputChannels Number of input channels
45 /// @param numOutputChannels Number of output channels
46 /// @param maxBlockSizeFrames Maximum block size in frames (samples)
47 virtual void prepare (double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames) = 0;
48
49 /// @brief Processes the audio
50 /// @details Depending on circumstances, this callback will either be called once to process an entire piece of audio from
51 /// start to finish, or called repeatedly, one block at a time (see @ref AudioTestBuilder::withBlockSize()).
52 /// All audio blocks except the last one are guaranteed to be equal to ```maxBlockSizeFrames``` set in @ref prepare() callback.
53 /// It is guaranteed that input and output buffers are equal in length in frames (samples) to each,
54 /// but they might have different number of channels. Use @ref supportsChannelLayout() to indicate
55 /// whether the effect supports a specific i/o configuration or not, as it will be called before @ref prepare().
56 /// It is guaranteed that ```envelopeBuffers``` will only contain the values for all attached envelopes for this instance of DSP
57 /// effect, and will not contain any data (including key with empty item) if there's no envelope attached to a specific parameter
58 /// ID in this effects's instance. It will never contain envelopes for IDs that get rejected by @ref supportsEnvelopeFor().
59 /// @note This method may be called in a replacing manner, i. e. ```input``` and ```output``` may be references to the same object.
60 /// @warning Remember that the very last block of audio is almost always smaller than the block size set in @ref prepare(), so be
61 /// careful with buffer bounds.
62 /// @note Note that this method does not have to be real-time safe, as all rendering always happens offline.
63 /// Also note that, unlike real-time audio applications, this method is called on the same thread as all others like @ref prepare().
64 /// @param input Input audio block
65 /// @param output Output audio block
66 /// @param envelopeBuffers Envelope values for this block, see @ref EnvelopeBuffers
67 virtual void process (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output, const EnvelopeBuffers& envelopeBuffers) = 0;
68
69 /// @brief Resets to initial state
70 /// @details Ideally should be implemented in a way that audio produced after resetting is identical to audio produced after instantiation
71 virtual void reset() = 0;
72
73 /// @brief Sets DSP value
74 /// @param paramId Some ID that your subclass understands;
75 /// use of enums is encouraged for readability
76 /// @param value Value of the param in an appropriate unit;
77 /// use of SI units is enocuraged (i.e. s instead of ms. Hz instead of kHz) to make better use of unit literals (see @ref Units)
78 /// @warning This method is only called to set a fixed value before processing, and is not called to do automation (via @ref hart::Envelope)
79 /// If you want your class to support automation for a specific parameter, override @ref supportsEnvelopeFor(), and then
80 /// use @c envelopeBuffers provided in @ref process() callback.
81 virtual void setValue (int paramId, double value) = 0;
82
83 /// @brief Retreives DSP value
84 /// @details Among other things, it can be used to retreive various readings like Gain Reduction measurements from your effect for further inspection
85 /// @param paramId Some ID that your subclass understands
86 /// @return The value of requested parameter in a unit that your subclass understands
87 /// @note This method is only intended for parameters that don't have an automation envelope attached to this specific instance.
88 /// To get values for automated parameters, use @c envelopeBuffers provided in @ref process() callback.
89 virtual double getValue (int paramId) const = 0;
90
91 /// @brief Tells the runner (host) whether this effect supports a specific i/o configuration.
92 /// @details It is guaranteed that the effect will not receive unsupported number of channels in @ref process().
93 /// However, it is not always to handle gracefully channel layout being unsupported, so in some circumstances
94 /// it can cause an exception or a test failure. This method is guaranteed to be called at least once before @ref prepare()
95 virtual bool supportsChannelLayout (size_t numInputChannels, size_t numOutputChannels) const = 0;
96
97 /// @brief Makes a text representation of this DSP effect for test failure outputs.
98 /// @details It is strongly encouraged to follow python's
99 /// <a href="https://docs.python.org/3/reference/datamodel.html#object.__repr__" target="_blank">repr()</a>
100 /// conventions for returned text - basically, put something like "MyClass(value1, value2)" (with no quotes)
101 /// into the stream whenever possible, or "<Readable info in angled brackets>" otherwise.
102 /// Also, use built-in stream manipulators like @ref dbPrecision wherever applicable.
103 /// Use @ref HART_DEFINE_GENERIC_REPRESENT() to get a basic implementation for this method.
104 /// @param[out] stream Output stream to write to
105 virtual void represent (std::ostream& stream) const = 0;
106
107 /// @brief Tells whether this effect accepts automation envelopes for a particular parameter
108 /// @param paramId Some ID that your subclass understands
109 /// @return true if your subclass can process automation for this parameter, false otherwise
110 virtual bool supportsEnvelopeFor (int /* paramId */) const { return false; }
111
112 /// @brief Tells whether this effect supports given sample rate
113 /// @details It is guaranteed to be called before @ref prepare()
114 /// @param sampleRateHz Sample rate in question
115 /// @return true if effect is capable of interpreting and processing in a given sample rate, false otherwise
116 virtual bool supportsSampleRate (double /* sampleRateHz */) const { return true; }
117
118 /// @brief Returns a smart pointer with a copy of this object
119 /// @details Just put one of those two macros into your class body, and your @ref copy() and @ref move() are sorted:
120 /// - @ref HART_DSP_DEFINE_COPY_AND_MOVE() for movable and copyable classes
121 /// - @ref HART_DSP_FORBID_COPY_AND_MOVE for non-movable and non-copyable classes
122 ///
123 /// Read their description, and choose one that fits your class.
124 /// You can, of course, make your own implementation, but you're not supposed to, unless you're doing something obscure.
125 virtual std::unique_ptr<DSP<SampleType>> copy() const = 0;
126
127 /// @brief Returns a smart pointer with a moved instance of this object
128 /// @details Just pick a macro to define it - see description for @ref copy() for details
129 virtual std::unique_ptr<DSP<SampleType>> move() = 0;
130
131 /// @brief Destructor
132 virtual ~DSP() = default;
133
134 /// @brief Default constructor
135 DSP() = default;
136
137 /// @brief Copies from another DSP effect instance
138 /// @details Attached automation envelopes are deep-copied
139 DSP (const DSP& other)
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 DSP (DSP&& other) noexcept
148 : m_envelopes (std::move (other.m_envelopes))
149 {
150 }
151
152 /// @brief Copies from another DSP effect instance
153 /// @details Attached automation envelopes are deep-copied
154 DSP& operator= (const DSP& other)
155 {
156 if (this == &other)
157 return *this;
158
159 for (auto& pair : other.m_envelopes)
160 m_envelopes.emplace (pair.first, pair.second->copy());
161
162 return *this;
163 }
164
165 /// @brief Move assignment
166 /// @details Attached automation envelopes are moved as well
167 DSP& operator= (DSP&& other) noexcept
168 {
169 if (this != &other)
170 m_envelopes = std::move (other.m_envelopes);
171
172 return *this;
173 }
174
175 /// @brief Adds envelope to a specific parameter by moving it
176 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
177 /// and only if it has returned ```true``` for this specific ```paramId```.
178 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
179 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
180 /// @param paramId Some ID that your subclass understands
181 /// @param envelope Envelope to be attached
182 /// @return Reference to itself for chaining
183 DSP& withEnvelope (int paramId, Envelope&& envelope)
184 {
185 if (! supportsEnvelopeFor(paramId))
186 HART_THROW_OR_RETURN (hart::UnsupportedError, std::string ("DSP doesn't support envelopes for param ID: ") + std::to_string (paramId), *this);
187
188 m_envelopes.emplace (paramId, hart::make_unique<Envelope> (std::move (envelope)));
189 return *this;
190 }
191
192 /// @brief Adds envelope to a specific parameter by copying it
193 /// @details Guaranteed to be called strictly after the @ref supportsEnvelopeFor() callback,
194 /// and only if it has returned ```true``` for this specific ```paramId```.
195 /// Can be chained together like ```myEffect.withEnvelope (someId, someEnvelope).withEnvelope (otherId, otherEnvelope)```.
196 /// If called multiple times for the same paramId, only last envelope for this ID will be used, all previous ones will be descarded.
197 /// @param paramId Some ID that your subclass understands
198 /// @param envelope Envelope to be attached
199 /// @return Reference to itself for chaining
200 DSP& withEnvelope (int paramId, const Envelope& envelope)
201 {
202 if (! supportsEnvelopeFor(paramId))
203 HART_THROW_OR_RETURN (hart::UnsupportedError, std::string ("DSP doesn't support envelopes for param ID: ") + std::to_string (paramId), *this);
204
205 m_envelopes.emplace (paramId, envelope.copy());
206 return *this;
207 }
208
209 /// @brief Checks if there's an automation envelope attached to a specific parameter
210 /// @details The envelopes are guaranteed to be attached strictly before @ref prepare()
211 /// callback, so by the time of the first @ref process() call consider the presence or
212 /// absence of envelope permanent.
213 /// @return Reference to itself for chaining
214 bool hasEnvelopeFor (int paramId)
215 {
216 return m_envelopes.find (paramId) != m_envelopes.end();
217 }
218
219 /// @brief Prepares all the attached envelopes and the effect itself for processing
220 /// @details This method is intended to be called by DSP hosts like @ref hart::AudioTestBuilder or @ref hart::Signal.
221 /// If you're making something that owns an instance of a DSP and needs it to generate audio,
222 /// you must call this method before calling @ref processWithEnvelopes().
223 /// You must also call @ref supportsChannelLayout() and @ref supportsSampleRate() before calling this method.
224 /// @attention If you're not making a custom host, you probably don't need to call this method.
225 void prepareWithEnvelopes (double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames)
226 {
227 m_envelopeBuffers.clear(); // TODO: Remove only unused buffers
228
229 for (auto& item : m_envelopes)
230 {
231 const int paramId = item.first;
232 m_envelopeBuffers.emplace (paramId, std::vector<double> (maxBlockSizeFrames));
233 }
234
235 hassert (m_envelopes.size() == m_envelopeBuffers.size());
236
237 for (auto& item : m_envelopeBuffers)
238 {
239 const int paramId = item.first;
240 auto& envelopeBuffer = item.second;
241
242 // Sanity checks
243 hassert (supportsEnvelopeFor (paramId) && "Envelope for this id is unsupported, yet there's an envelope buffer allocated for it");
244 hassert (hasEnvelopeFor (paramId) && "Envelope for this param id is not attached, yet there's an envelope buffer allocated for it");
245
246 if (envelopeBuffer.size() != maxBlockSizeFrames)
247 envelopeBuffer.resize (maxBlockSizeFrames);
248 }
249
250 prepare (sampleRateHz, numInputChannels, numOutputChannels, maxBlockSizeFrames);
251 }
252
253 /// @brief Renders all the automation envelopes and processes the audio
254 /// @details This method is intended to be called by DSP hosts like @ref hart::AudioTestBuilder @ref hart::Signal.
255 /// If you're making something that owns an instance of a Signal and needs it to generate audio,
256 /// you must call it after calling @ref prepareWithEnvelopes().
257 /// @attention If you're not making a custom host, you probably don't need to call this method.
258 /// @param input Input audio block
259 /// @param output Output audio block
260 void processWithEnvelopes (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output)
261 {
262 for (auto& item : m_envelopeBuffers)
263 {
264 const int paramId = item.first;
265 auto& envelopeBuffer = item.second;
266
267 // Sanity checks
268 hassert (supportsEnvelopeFor (paramId) && "Envelope for this id is unsupported, yet there's an envelope buffer allocated for it");
269 hassert (hasEnvelopeFor (paramId) && "Envelope for this param id is not attached, yet there's an envelope buffer allocated for it");
270 hassert (input.getNumFrames() <= envelopeBuffer.size() && "Envelope Buffers were not allocated properly for this buffer size");
271
272 // Render envelope values
273 getValues (paramId, envelopeBuffer.size(), envelopeBuffer);
274 }
275
276 process (input, output, m_envelopeBuffers);
277 }
278
279 /// @brief Helper for template resolution
280 /// @private
281 using SampleTypePublicAlias = SampleType;
282
283private:
284 std::unordered_map<int, std::unique_ptr<Envelope>> m_envelopes;
285 EnvelopeBuffers m_envelopeBuffers;
286
287 /// @brief Gets sample-accurate automation envelope values for a specific parameter
288 /// @param[in] paramId Some ID that your subclass understands
289 /// @param[in] blockSize Buffer size in frames, should be the same as ```input```/```output```'s size in @ref process()
290 /// @param[out] valuesOutput Container to get filled with the rendered automation values
291 void getValues (int paramId, size_t blockSize, std::vector<double>& valuesOutput)
292 {
293 if (valuesOutput.size() < blockSize)
294 {
295 HART_WARNING ("Make sure to configure your envelope container size before processing audio");
296 valuesOutput.resize (blockSize);
297 }
298
299 if (! hasEnvelopeFor (paramId))
300 {
301 const double value = getValue (paramId);
302 std::fill (valuesOutput.begin(), valuesOutput.end(), value);
303 }
304 else
305 {
306 m_envelopes[paramId]->renderNextBlock (blockSize, valuesOutput);
307 }
308 }
309};
310
311/// @brief Prints readable text representation of the DSP object into the I/O stream
312/// @relates DSP
313/// @ingroup DSP
314template <typename SampleType>
315inline std::ostream& operator<< (std::ostream& stream, const DSP<SampleType>& dsp)
316{
317 // TODO: Represent with the envelopes
318 dsp.represent (stream);
319 return stream;
320}
321
322/// @brief Defines @ref hart::DSP::copy() and @ref hart::DSP::move() methods
323/// @details Put this into your class body's ```public``` section if either is true:
324/// - Your class is trivially copyable and movable
325/// - You have your Rule Of Five methods explicitly defined in this class
326/// (see <a href="https://en.cppreference.com/w/cpp/language/rule_of_three.html" target="_blank">Rule Of Three/Five/Zero</a>)
327///
328/// If neither of those is true, or you're unsure, use @ref HART_DSP_FORBID_COPY_AND_MOVE instead
329///
330/// Despite returning a smart pointer to an abstract DSP class, those two methods must construct
331/// an object of a specific class, hence the mandatory boilerplate methods - sorry!
332/// @param ClassName Name of your class
333/// @ingroup DSP
334#define HART_DSP_DEFINE_COPY_AND_MOVE(ClassName)
335 std::unique_ptr<DSP<SampleType>> copy() const override {
336 return hart::make_unique<ClassName> (*this);
337 }
338 std::unique_ptr<DSP<SampleType>> move() override {
339 return hart::make_unique<ClassName> (std::move (*this));
340 }
341
342/// @brief Forbids @ref hart::DSP::copy() and @ref hart::DSP::move() methods
343/// @details Put this into your class body's ```public``` section if either is true:
344/// - Your class is not trivially copyable and movable
345/// - You don't want to trouble yourself with implementing move and copy semantics for your class
346///
347/// Otherwise, use @ref HART_DSP_DEFINE_COPY_AND_MOVE() instead.
348/// Obviously, you won't be able to pass your class to the host
349/// by reference, copy or explicit move, but you still can pass
350/// it wrapped into a smart pointer like so:
351/// ```cpp
352/// processAudioWith (hart::make_unique<MyDspType>()).withThis().withThat().process();
353/// ```
354/// But it's still better to get your move and copy semantics figured out - this is a
355/// perfect chance to stress-test your effect's resource management, among other things!
356/// @ingroup DSP
357#define HART_DSP_FORBID_COPY_AND_MOVE
358 std::unique_ptr<DSP<SampleType>> copy() const override {
359 static_assert(false, "This DSP cannot be copied");
360 return nullptr;
361 }
362 std::unique_ptr<DSP<SampleType>> move() override {
363 static_assert(false, "This DSP cannot be moved");
364 return nullptr;
365 }
366
367} // namespace hart
368
369/// @private
370#define HART_DSP_DECLARE_ALIASES_FOR(ClassName)
371 namespace aliases_float{
372 using ClassName = hart::ClassName<float>;
373 }
374 namespace aliases_double{
375 using ClassName = hart::ClassName<double>;
376 }
Base for DSP effects.
Definition hart_dsp.hpp:34
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:225
virtual void reset()=0
Resets to initial state.
virtual void process(const AudioBuffer< SampleType > &input, AudioBuffer< SampleType > &output, const EnvelopeBuffers &envelopeBuffers)=0
Processes the audio.
virtual void represent(std::ostream &stream) const =0
Makes a text representation of this DSP effect for test failure outputs.
virtual double getValue(int paramId) const =0
Retreives DSP value.
DSP & withEnvelope(int paramId, const Envelope &envelope)
Adds envelope to a specific parameter by copying it.
Definition hart_dsp.hpp:200
DSP & withEnvelope(int paramId, Envelope &&envelope)
Adds envelope to a specific parameter by moving it.
Definition hart_dsp.hpp:183
DSP()=default
Default constructor.
virtual ~DSP()=default
Destructor.
DSP(const DSP &other)
Copies from another DSP effect instance.
Definition hart_dsp.hpp:139
DSP & operator=(const DSP &other)
Copies from another DSP effect instance.
Definition hart_dsp.hpp:154
virtual bool supportsSampleRate(double) const
Tells whether this effect supports given sample rate.
Definition hart_dsp.hpp:116
virtual std::unique_ptr< DSP< SampleType > > move()=0
Returns a smart pointer with a moved instance of this object.
void processWithEnvelopes(const AudioBuffer< SampleType > &input, AudioBuffer< SampleType > &output)
Renders all the automation envelopes and processes the audio.
Definition hart_dsp.hpp:260
DSP & operator=(DSP &&other) noexcept
Move assignment.
Definition hart_dsp.hpp:167
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.
bool hasEnvelopeFor(int paramId)
Checks if there's an automation envelope attached to a specific parameter.
Definition hart_dsp.hpp:214
virtual std::unique_ptr< DSP< SampleType > > copy() const =0
Returns a smart pointer with a copy of this object.
DSP(DSP &&other) noexcept
Move constructor.
Definition hart_dsp.hpp:147
virtual bool supportsEnvelopeFor(int) const
Tells whether this effect accepts automation envelopes for a particular parameter.
Definition hart_dsp.hpp:110
Represents an Envelope curve for DSP parameters.
virtual void renderNextBlock(size_t blockSize, std::vector< double > &valuesOutput)=0
virtual std::unique_ptr< Envelope > copy() const =0
std::unique_ptr< ObjectType > make_unique(Args &&... args)
std::make_unique() replacement for C++11
#define HART_WARNING(message)
#define hassert(condition)
#define HART_THROW_OR_RETURN(ExceptionType, message, returnValue)