HART  0.1.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_signal.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath> // sin()
5#include <cstdint>
6#include <memory>
7#include <random>
8#include <string>
9#include <vector>
10
12#include "dsp/hart_dsp.hpp"
14
15/// @defgroup Signals Signals
16/// @brief Generate signals
17
18namespace hart {
19
20/// @brief Base class for signals
21/// @ingroup Signals
22/// @tparam SampleType Type of values that will be generated, typically ```float``` or ```double```
23template<typename SampleType>
24class Signal
25{
26public:
27 /// @brief Default constructor
28 Signal() = default;
29
30 /// @brief Copies other signal
31 Signal (const Signal& other):
32 m_numChannels(other.m_numChannels)
33 {
34 if (other.dspChain.size() == 0)
35 return;
36
37 dspChain.reserve (dspChain.size());
38
39 for (auto& dsp : other.dspChain)
40 dspChain.push_back (dsp->copy());
41 }
42
43 /// @brief Moves from other signal
44 Signal (Signal&& other) noexcept:
45 m_numChannels (other.m_numChannels),
46 dspChain (std::move (other.dspChain))
47 {
48 other.m_numChannels = 0;
49 }
50
51 /// @brief Destructor
52 virtual ~Signal() = default;
53
54 /// @brief Copies from other signal
55 Signal& operator= (const Signal& other)
56 {
57 if (this == &other)
58 return *this;
59
60 m_numChannels = other.m_numChannels;
61 dspChain.clear();
62
63 if (other.dspChain.size() == 0)
64 return *this;
65
66 for (auto& dsp : other.dspChain)
67 dspChain.push_back (dsp->copy());
68
69 return *this;
70 }
71
72 /// @brief Moves from other signal
73 Signal& operator= (Signal&& other) noexcept
74 {
75 if (this == &other)
76 return *this;
77
78 m_numChannels = other.m_numChannels;
79 dspChain = std::move (other.dspChain);
80 other.m_numChannels = 0;
81
82 return *this;
83 }
84
85 /// @brief Tells the host whether this Signal is capable of generating audio for a certain amount of cchannels
86 /// @details It is guaranteed that the signal will not receive unsupported number of channels in @ref renderNextBlock().
87 /// This method is guaranteed to be called at least once before @ref prepare()
88 /// @note This method should only care about the Signal itself, and not the attached effects in DSP chain - they'll be queried separately
89 /// @param numChannels Number of output channels that will need to be filled
90 /// @return true if signal is capable of filling this many channels with audio, false otherwise
91 virtual bool supportsNumChannels (size_t /* numChannels */) const { return true; };
92
93 /// @brief Tells whether this Signal supports given sample rate
94 /// @details It is guaranteed to be called before @ref prepare()
95 /// @note This method should only care about the Signal itself, and not the attached effects in DSP chain - they'll be queried separately
96 /// @param sampleRateHz sample rate at which the audio should be generated
97 /// @return true if signal is capable of generating audio at a given sample rate, false otherwise
98 virtual bool supportsSampleRate (double /* sampleRateHz */) const { return true; }
99
100 /// @brief Prepare the signal for rendering
101 /// @details This method is guaranteed to be called after @ref supportsNumChannels() and supportsSampleRate(),
102 /// but before @ref renderNextBlock().
103 /// It is guaranteed that ```numChannels``` obeys supportsNumChannels() preferences, same with ```sampleRateHz```
104 /// and @ref supportsSampleRate(). It is guaranteed that all subsequent renderNextBlock() calls will be in line
105 /// with the arguments received in this callback.
106 /// @param sampleRateHz sample rate at which the audio should be generated
107 /// @param numOutputChannels Number of output channels to be filled
108 /// @param maxBlockSizeFrames Maximum block size in frames (samples)
109 virtual void prepare (double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames) = 0;
110
111 /// @brief Renders next block audio for the signal
112 /// @details Depending on circumstances, this callback will either be called once to generate an entire piece of audio from
113 /// start to finish, or called repeatedly, one block at a time.
114 /// This method is guaranteed to be called strictly after @ref prepare(), or not called at all.
115 /// Number of channels and max block size are guaranteed to be in line with the ones set by prepare() callback.
116 /// Assume sample rate to always be equal to the one received in the last @ref prepare() callback.
117 /// All audio blocks except the last one are guaranteed to be equal to ```maxBlockSizeFrames``` set in @ref prepare() callback.
118 /// @warning Remember that the very last block of audio is almost always smaller than the block size set in @ref prepare(), so be
119 /// careful with buffer bounds.
120 /// @note Note that this method does not have to be real-time safe, as all rendering always happens offline.
121 /// Also note that, unlike real-time audio applications, this method is called on the same thread as all others like @ref prepare().
122 /// @param output Output audio block
123 virtual void renderNextBlock (AudioBuffer<SampleType>& output) = 0;
124
125 /// @brief Resets the Signal to initial state
126 /// @details Ideally should be implemented in a way that audio produced after resetting is identical to audio produced after instantiation
127 virtual void reset() = 0;
128
129 /// @brief Returns a smart pointer with a copy of this object
130 /// @details Just put one of those two macros into your class body, and your @ref copy() and @ref move() are sorted:
131 /// - @ref HART_SIGNAL_DEFINE_COPY_AND_MOVE() for movable and copyable classes
132 /// - @ref HART_SIGNAL_FORBID_COPY_AND_MOVE for non-movable and non-copyable classes
133 ///
134 /// Read their description, and choose one that fits your class.
135 /// You can, of course, make your own implementation, but you're not supposed to, unless you're doing something obscure.
136 virtual std::unique_ptr<Signal<SampleType>> copy() const = 0;
137
138 /// @brief Returns a smart pointer with a moved instance of this object
139 /// @details Just pick a macro to define it - see description for @ref copy() for details
140 virtual std::unique_ptr<Signal<SampleType>> move() = 0;
141
142 /// @brief Makes a text representation of this Signal for test failure outputs.
143 /// @details It is strongly encouraged to follow python's
144 /// <a href="https://docs.python.org/3/reference/datamodel.html#object.__repr__" target="_blank">repr()</a>
145 /// conventions for returned text - basically, put something like "MyClass(value1, value2)" (with no quotes)
146 /// into the stream whenever possible, or "<Readable info in angled brackets>" otherwise.
147 /// Also, use built-in stream manipulators like @ref dbPrecision wherever applicable.
148 /// Use @ref HART_DEFINE_GENERIC_REPRESENT() to get a basic implementation for this method.
149 /// @param[out] stream Output stream to write to
150 virtual void represent (std::ostream& stream) const = 0;
151
152 /// @brief Adds a DSP effect to the end of signal's DSP chain by copying it
153 /// @note For DSP object that do not support copying or moving, use version of this method that takes a ```unique_ptr``` instead
154 /// @param dsp A DSP effect instance
155 Signal& followedBy (const DSP<SampleType>& dsp)
156 {
157 dspChain.emplace_back (dsp.copy());
158 return *this;
159 }
160
161 /// @brief Adds a DSP effect to the end of signal's DSP chain by transfering a smart pointer
162 /// @note For DSP object that do not support copying or moving, use version of this method that takes a ```unique_ptr``` instead
163 /// @param dsp A DSP effect instance
164 Signal& followedBy (std::unique_ptr<DSP<SampleType>> dsp)
165 {
166 dspChain.emplace_back (std::move (dsp));
167 return *this;
168 }
169
170 // TODO: Add check if rvalue
171 /// @brief Adds a DSP effect to the end of signal's DSP chain by moving it
172 /// @note For DSP object that do not support copying or moving, use version of this method that takes a ```unique_ptr``` instead
173 /// @param dsp A DSP effect instance
174
175 template <
176 typename DerivedDSP,
177 typename = typename std::enable_if<
178 std::is_base_of<
179 DSP<SampleType>,
180 typename std::decay<DerivedDSP>::type
181 >::value
182 >::type
183 >
184 Signal& followedBy (DerivedDSP&& dsp)
185 {
186 dspChain.emplace_back (
187 hart::make_unique<typename std::decay<DerivedDSP>::type> (std::forward<DerivedDSP> (dsp))
188 );
189 return *this;
190 }
191
192 /// @brief Prepares the signal and all attached effects in the DSP chain for rendering
193 /// @details This method is intended to be called by Signal hosts like AudioTestBuilder or Matcher.
194 /// If you're making something that owns an instance of a Signal and needs it to generate audio,
195 /// like a custom Matcher, you must call this method before calling @ref renderNextBlockWithDSPChain().
196 /// You must also call @ref supportsNumChannels() and @ref supportsSampleRate() before calling this method.
197 /// @attention If you're not making a custom host, you probably don't need to call this method.
198 void prepareWithDSPChain (double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames)
199 {
200 prepare (sampleRateHz, numOutputChannels, maxBlockSizeFrames);
201 const size_t numInputChannels = numOutputChannels;
202
203 // TODO: Check if all the effects in the chain support those settings first
204
205 for (auto& dsp : dspChain)
206 {
207 if (! dsp->supportsChannelLayout (numInputChannels, numOutputChannels))
208 HART_THROW_OR_RETURN_VOID (ChannelLayoutError, "Not all DSP in the Signal's DSP chain support its channel layout");
209
210 if (! dsp->supportsSampleRate (sampleRateHz))
211 HART_THROW_OR_RETURN_VOID (hart::SampleRateError, "Not all DSP in the Signal's DSP chain support its sample rate");
212
213 dsp->prepareWithEnvelopes (sampleRateHz, numInputChannels, numOutputChannels, maxBlockSizeFrames);
214 }
215 }
216
217 /// @brief Renders next block audio for the signal and all the effects in the DSP chain
218 /// @details This method is intended to be called by Signal hosts like AudioTestBuilder or Matcher
219 /// If you're making something that owns an instance of a Signal and needs it to generate audio,
220 /// like a custom Matcher, you must call it after calling @ref prepareWithDSPChain().
221 /// @attention If you're not making a custom host, you probably don't need to call this method.
222 void renderNextBlockWithDSPChain (AudioBuffer<SampleType>& output)
223 {
224 renderNextBlock (output);
225 AudioBuffer<SampleType>& inputReplacing = output;
226
227 for (auto& dsp : dspChain)
228 dsp->processWithEnvelopes (inputReplacing, output);
229 }
230
231 /// @brief Resets to Signal and all the effects attached to its DSP chain to initial state
232 /// @details This method is intended to be called by hosts like AudioTestBuilder or Matcher.
233 /// If you're not making a custom host, you probably don't need this method.
234 virtual void resetWithDSPChain()
235 {
236 reset();
237
238 for (auto& dsp : dspChain)
239 dsp->reset();
240 }
241
242 /// @brief Makes a text representation of this signal and its entire signal chain for test failure outputs.
243 /// @details Used by "<<" operator
244 /// @private
245 /// @param[out] stream Output stream to write to
246 void representWithDSPChain (std::ostream& stream) const
247 {
248 represent (stream);
249
250 for (const auto& dsp : dspChain)
251 stream << " >> " << *dsp;
252 }
253
254 /// @brief Helper for template resolution
255 /// @private
256 using m_SampleType = SampleType;
257
258protected:
259 void setNumChannels (size_t numChannels)
260 {
261 m_numChannels = numChannels;
262 }
263
265 {
266 return m_numChannels;
267 }
268
269private:
270 size_t m_numChannels = 1;
271 std::vector<std::unique_ptr<DSP<SampleType>>> dspChain;
272};
273
274/// @brief Prints readable text representation of the Signal object into the I/O stream
275/// @relates Signal
276/// @ingroup Signals
277template<typename SampleType>
278std::ostream& operator<< (std::ostream& stream, const Signal<SampleType>& signal)
279{
280 signal.representWithDSPChain (stream);
281 return stream;
282}
283
284/// @brief Adds a DSP effect to the end of signal's DSP chain by moving it
285/// @relates Signal
286/// @ingroup Signals
287template<typename SampleType,
288 typename DerivedDSP, typename std::enable_if<std::is_base_of<DSP<SampleType>, typename std::decay<DerivedDSP>::type>::value>::type>
289Signal<SampleType>& operator>> (Signal<SampleType>& signal, DerivedDSP&& dsp)
290{
291 return signal.followedBy (std::move (dsp));
292}
293
294/// @brief Adds a DSP effect to the end of signal's DSP chain by copying it
295/// @relates Signal
296/// @ingroup Signals
297template<typename SampleType>
298Signal<SampleType>& operator>> (Signal<SampleType>& signal, const DSP<SampleType>& dsp)
299{
300 return signal.followedBy (dsp);
301}
302
303/// @brief Adds a DSP effect to the end of signal's DSP chain by copying it
304/// @relates Signal
305/// @ingroup Signals
306template<typename SampleType>
307Signal<SampleType>&& operator>> (Signal<SampleType>&& signal, const DSP<SampleType>& dsp)
308{
309 return std::move (signal.followedBy (dsp));
310}
311
312/// @brief Adds a DSP effect to the end of signal's DSP chain by transfering it
313/// @relates Signal
314/// @ingroup Signals
315template<typename SampleType>
316Signal<SampleType>& operator>> (Signal<SampleType>& signal, std::unique_ptr<DSP<SampleType>> dsp)
317{
318 signal.followedBy (std::move (dsp));
319 return signal;
320}
321
322/// @brief Adds a DSP effect to the end of signal's DSP chain by transfering it
323/// @relates Signal
324/// @ingroup Signals
325template<typename SampleType>
326Signal<SampleType>&& operator>> (Signal<SampleType>&& signal, std::unique_ptr<DSP<SampleType>> dsp)
327{
328 signal.followedBy (std::move (dsp));
329 return std::move (signal);
330}
331
332} // namespace hart
333
334/// @brief Defines @ref hart::Signal::copy() and @ref hart::Signal::move() methods
335/// @details Put this into your class body's ```public``` section if either is true:
336/// - Your class is trivially copyable and movable
337/// - You have your Rule Of Five methods explicitly defined in this class
338/// (see <a href="https://en.cppreference.com/w/cpp/language/rule_of_three.html" target="_blank">Rule Of Three/Five/Zero</a>)
339///
340/// If neither of those is true, or you're unsure, use @ref HART_SIGNAL_FORBID_COPY_AND_MOVE instead
341///
342/// Despite returning a smart pointer to an abstract Signal class, those two methods must construct
343/// an object of a specific class, hence the mandatory boilerplate methods - sorry!
344/// @param ClassName Name of your class
345/// @ingroup Signals
346#define HART_SIGNAL_DEFINE_COPY_AND_MOVE(ClassName)
347 std::unique_ptr<Signal<SampleType>> copy() const override {
348 return hart::make_unique<ClassName> (*this);
349 }
350 std::unique_ptr<Signal<SampleType>> move() override {
351 return hart::make_unique<ClassName> (std::move (*this));
352 }
353
354/// @brief Forbids @ref hart::Signal::copy() and @ref hart::Signal::move() methods
355/// @details Put this into your class body's ```public``` section if either is true:
356/// - Your class is not trivially copyable and movable
357/// - You don't want to trouble yourself with implementing move and copy semantics for your class
358///
359/// Otherwise, use @ref HART_SIGNAL_DEFINE_COPY_AND_MOVE() instead.
360/// Obviously, you won't be able to pass your class to the host
361/// by reference, copy or explicit move, but you still can pass
362/// it wrapped into a smart pointer like so:
363/// ```cpp
364/// processAudioWith (MyDSP())
365/// .withInputSignal(hart::make_unique<MyDspType>()) // As input signal
366/// .expectTrue (EqualsTo (hart::make_unique<MyDspType>())) // As reference signal
367/// .process();
368/// ```
369/// @ingroup Signals
370#define HART_SIGNAL_FORBID_COPY_AND_MOVE
371 std::unique_ptr<Signal<SampleType>> copy() const override {
372 static_assert(false, "This Signal cannot be copied");
373 return nullptr;
374 }
375 std::unique_ptr<Signal<SampleType>> move() override {
376 static_assert(false, "This Signal cannot be moved");
377 return nullptr;
378 }
379
380/// @private
381#define HART_SIGNAL_DECLARE_ALIASES_FOR(ClassName)
382 namespace aliases_float{
383 using ClassName = hart::ClassName<float>;
384 }
385 namespace aliases_double{
386 using ClassName = hart::ClassName<double>;
387 }
Base for DSP effects.
Definition hart_dsp.hpp:34
Base class for signals.
virtual void reset()=0
Resets the Signal to initial state.
virtual void represent(std::ostream &stream) const =0
Makes a text representation of this Signal for test failure outputs.
void renderNextBlockWithDSPChain(AudioBuffer< SampleType > &output)
Renders next block audio for the signal and all the effects in the DSP chain.
virtual void resetWithDSPChain()
Resets to Signal and all the effects attached to its DSP chain to initial state.
Signal & followedBy(const DSP< SampleType > &dsp)
Adds a DSP effect to the end of signal's DSP chain by copying it.
Signal(const Signal &other)
Copies other signal.
virtual void renderNextBlock(AudioBuffer< SampleType > &output)=0
Renders next block audio for the signal.
Signal & followedBy(std::unique_ptr< DSP< SampleType > > dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering a smart pointer.
void prepareWithDSPChain(double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames)
Prepares the signal and all attached effects in the DSP chain for rendering.
void setNumChannels(size_t numChannels)
virtual ~Signal()=default
Destructor.
virtual bool supportsNumChannels(size_t) const
Tells the host whether this Signal is capable of generating audio for a certain amount of cchannels.
virtual bool supportsSampleRate(double) const
Tells whether this Signal supports given sample rate.
Signal(Signal &&other) noexcept
Moves from other signal.
Signal & operator=(Signal &&other) noexcept
Moves from other signal.
virtual std::unique_ptr< Signal< SampleType > > copy() const =0
Returns a smart pointer with a copy of this object.
size_t getNumChannels()
Signal & followedBy(DerivedDSP &&dsp)
Adds a DSP effect to the end of signal's DSP chain by moving it.
Signal()=default
Default constructor.
Signal & operator=(const Signal &other)
Copies from other signal.
virtual void prepare(double sampleRateHz, size_t numOutputChannels, size_t maxBlockSizeFrames)=0
Prepare the signal for rendering.
virtual std::unique_ptr< Signal< SampleType > > move()=0
Returns a smart pointer with a moved instance of this object.
Signal< SampleType > && operator>>(Signal< SampleType > &&signal, std::unique_ptr< DSP< SampleType > > dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering it.
Signal< SampleType > & operator>>(Signal< SampleType > &signal, const DSP< SampleType > &dsp)
Adds a DSP effect to the end of signal's DSP chain by copying it.
Signal< SampleType > & operator>>(Signal< SampleType > &signal, DerivedDSP &&dsp)
Adds a DSP effect to the end of signal's DSP chain by moving it.
Signal< SampleType > && operator>>(Signal< SampleType > &&signal, const DSP< SampleType > &dsp)
Adds a DSP effect to the end of signal's DSP chain by copying it.
Signal< SampleType > & operator>>(Signal< SampleType > &signal, std::unique_ptr< DSP< SampleType > > dsp)
Adds a DSP effect to the end of signal's DSP chain by transfering it.
#define HART_THROW_OR_RETURN_VOID(ExceptionType, message)