HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_dsp_sequence.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <cstddef> // ptrdiff_t
4#include <memory>
5#include <sstream>
6#include <utility>
7#include <vector>
8
9#include "hart_dsp.hpp"
11#include "hart_utils.hpp" // make_unique()
12
13namespace hart
14{
15
16/// @brief A DSP unit that renders audio through a linear sequence of DSP instances
17/// @details It's a container-like DSP subclass that owns a linear chain of
18/// other DSP units and applies them one after another.
19/// The first DSP in the chain processes the input buffer into the output buffer,
20/// while all following DSP units process the output buffer in-place.
21/// Nested DSP sequences are fully supported, since `DSPSequence` itself is a
22/// normal DSP subclass.
23/// Empty sequences are also valid, and just output silence (zeros).
24///
25/// This class is intended to be constructed via the `HART_DSP_SEQUENCE()` macro:
26/// @code
27/// auto myDspChain = HART_DSP_SEQUENCE (
28/// GainDb (-3_dB) >> HardClip (-1_dB) >> TimeShift (5_ms)
29/// );
30/// @endcode
31/// but using the ctor directly is also appropriate in some cases, for example,
32/// for creating an empty sequence, and then filling it using some loop.
33///
34/// The class also supports runtime mutation:
35/// - adding DSP units
36/// - removing (extracting) DSP units
37/// - peeking at DSP units without extracting them
38///
39/// @note
40/// Although the class can be constructed directly, the macro syntax is usually
41/// more concise and preferred in tests.
42/// @attention Doesn't support setting per-channel flags, such as
43/// `DSPSequence::atChannel ( {x, y, z} )` at the sequence level - all channels
44/// will still be processed regardless. However, using per-channel flags on the
45/// individual DSP instances works as it should.
46/// @see `HART_DSP_SEQUENCE()`, `HART_DSP_SEQUENCE_T()`
47/// @ingroup DSP
48template <typename SampleType>
50 public hart::DSP<SampleType, DSPSequence<SampleType>>
51{
52public:
53 /// @brief Creates a sequence of DSP processors
54 /// @attention Consider using `HART_DSP_SEQUENCE()` or `HART_DSP_SEQUENCE_T()`
55 /// instead of using this ctor directly, unless you intend to create an
56 /// empty sequence.
57 /// @param dspChain A vector with DSP objects. It will be moved into the DSPSequence,
58 /// along with all DSP instances.
59 DSPSequence (std::vector<std::unique_ptr<DSPBase<SampleType>>> dspChain) :
60 m_dspChain (std::move (dspChain))
61 {
62 }
63
64 /// @brief Destructor
65 ~DSPSequence() override = default;
66
67 /// @brief No copy construction
68 /// @details Copy is still supported, but you have to call `copy()` method explicitly
69 DSPSequence (const DSPSequence&) = delete;
70
71 /// @brief Moves DSP chain from another DSP sequence
72 DSPSequence (DSPSequence&&) noexcept = default;
73
74 /// @brief No copy assignment
75 /// @details Copy is still supported, but you have to call `copy()` method explicitly
76 DSPSequence& operator= (const DSPSequence&) = delete;
77
78 /// @brief Moves DSP chain from another DSP sequence
79 DSPSequence& operator= (DSPSequence&&) noexcept = default;
80
81 void prepare (double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames) override
82 {
83 for (auto& dsp : m_dspChain)
84 dsp->prepareWithEnvelopes (sampleRateHz, numInputChannels, numOutputChannels, maxBlockSizeFrames);
85 }
86
87 void process (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output, const EnvelopeBuffers& /* envelopeBuffers */, ChannelFlags /* channelsToProcess */) override
88 {
89 if (m_dspChain.empty())
90 {
91 // No DSP - no audio. Bye.
92 // Bypass could also be appropriate, but too tricky with assymetrical channel layouts
93 output.clear();
94 return;
95 }
96
97 // First DSP - non-replacing processing
98 m_dspChain[0]->processWithEnvelopes (input, output);
99
100 if (m_dspChain.size() == 1)
101 return;
102
103 // All following DSPs - replacing processing
104 const AudioBuffer<SampleType>& inputReplacing = output;
105
106 // TODO: Merge DSPSequence's channel flags with those of nested effects
107
108 for (size_t i = 1; i < m_dspChain.size(); ++i)
109 m_dspChain[i]->processWithEnvelopes (inputReplacing, output);
110 }
111
112 bool supportsSampleRate (double sampleRate) const override
113 {
114 for (auto& dsp : m_dspChain)
115 if (! dsp->supportsSampleRate (sampleRate))
116 return false;
117
118 return true;
119 }
120
121 bool supportsChannelLayout (size_t numInputChannels, size_t numOutputChannels) const override
122 {
123 for (auto& dsp : m_dspChain)
124 if (! dsp->supportsChannelLayout (numInputChannels, numOutputChannels))
125 return false;
126
127 return true;
128 }
129
130 virtual bool supportsEnvelopeFor (int /* paramId */) const override { return false; }
131 virtual void setValue (int /* paramId */, double /* value */) override {}
132
133 void represent (std::ostream& stream) const override
134 {
135 stream << "HART_DSP_SEQUENCE (";
136
137 for (size_t i = 0; i < m_dspChain.size(); ++i)
138 {
139 if (i != 0)
140 stream << " >> ";
141
142 m_dspChain[i]->represent (stream);
143 }
144
145 stream << ')';
146 }
147
148 std::unique_ptr<DSPBase<SampleType>> copy() const override
149 {
150 std::vector<std::unique_ptr<DSPBase<SampleType>>> dspCopies;
151 dspCopies.reserve (m_dspChain.size());
152
153 for (size_t i = 0; i < m_dspChain.size(); ++i)
154 {
155 auto dspCopy = m_dspChain[i]->copy();
156 if (! dspCopy)
159 "DSPSequence::copy() failed because one of the contained DSP units is not copyable",
160 nullptr);
161
162 dspCopies.push_back (std::move (dspCopy));
163 }
164
165 return hart::make_unique<DSPSequence> (std::move (dspCopies));
166 }
167
168 /// @brief Returns the number of DSP elements in the sequence
169 /// @return Number of elements in the DSP chain
170 size_t size() const noexcept
171 {
172 return m_dspChain.size();
173 }
174
175 /// @brief Appends an element to the end of DSP chain
176 /// @details The element will be owned by the DSPSequence. You can get it back
177 /// by using `pop()` method, or peek by using square brackets `[]` operator.
178 /// @param dsp DSP element ot be appended. Must be a `hart::DSP` subclass.
179 template<typename DerivedDSP,
180 typename = typename std::enable_if<
181 ! std::is_lvalue_reference<DerivedDSP>::value
182 && std::is_base_of<
183 DSPBase<SampleType>,
184 typename std::decay<DerivedDSP>::type
185 >::value
186 >::type>
187 void append (DerivedDSP&& dsp)
188 {
189 m_dspChain.emplace_back (dsp.move());
190 }
191
192 /// @brief Appends an element to the end of DSP chain
193 /// @details The smart pointer will be owned by the DSPSequence. You can get it back
194 /// by using `pop()` method, or peek by using square brackets `[]` operator.
195 /// @param dsp Smart pointer with a DSP element ot be appended
196 void append (std::unique_ptr<DSPBase<SampleType>> dsp)
197 {
198 if (dsp == nullptr)
199 HART_THROW_OR_RETURN_VOID (hart::NullPointerError, "Appending nullptr DSP is not allowed");
200
201 m_dspChain.emplace_back (std::move (dsp));
202 }
203
204 /// @brief Accesses a specific element in the DSP sequence (mutable version)
205 /// @param index Index of the element. Can be negative. For non-negative values, it's a usual 0-based index.
206 /// For negative values, it's counted from the end, e.g. "-1" is the last element, "-2" is the second to last etc.
207 /// @throws hart::IndexError if the index is out of range
208 /// @return A pointer to the DSP instance
209 DSPBase<SampleType>* operator[] (long long int index)
210 {
211 return m_dspChain.at (normalizeIndex (index)).get();
212 }
213
214 /// @brief Accesses a specific element in the DSP sequence (immutable version)
215 /// @param index Index of the element. Can be negative. For non-negative values, it's a usual 0-based index.
216 /// For negative values, it's counted from the end, e.g. "-1" is the last element, "-2" is the second to last etc.
217 /// @throws hart::IndexError if the index is out of range
218 /// @return A pointer to the DSP instance
219 const DSPBase<SampleType>* operator[] (long long int index) const
220 {
221 return m_dspChain.at (normalizeIndex (index)).get();
222 }
223
224 /// @brief Extracts a specific element in the DSP chain by removing it
225 /// @param index Index of the element. Can be negative. For non-negative values, it's a usual 0-based index.
226 /// For negative values, it's counted from the end, e.g. "-1" is the last element, "-2" is the second to last etc.
227 /// @throws hart::IndexError if the index is out of range
228 /// @return A smart pointer with the DSP instance
229 std::unique_ptr<DSPBase<SampleType>> pop (long long int index = -1)
230 {
231 size_t normalizedIndex = normalizeIndex (index);
232 auto iterator = m_dspChain.begin() + static_cast<std::ptrdiff_t> (normalizedIndex);
233
234 std::unique_ptr<DSPBase<SampleType>> poppedDSP = std::move (*iterator);
235 m_dspChain.erase (iterator);
236 return poppedDSP;
237 }
238
239private:
240 size_t normalizeIndex (long long int index) const
241 {
242 const long long int size = static_cast<long long int> (m_dspChain.size());
243 const long long int normalizedIndex = index < 0 ? size + index : index;
244
245 if (normalizedIndex < 0 || normalizedIndex >= size)
246 HART_THROW_OR_RETURN (hart::IndexError, "DSPSequence index out of range", 0);
247
248 return static_cast<size_t> (normalizedIndex);
249 }
250
251 std::vector<std::unique_ptr<DSPBase<SampleType>>> m_dspChain;
252};
253
254/// @brief Internal helper used by `HART_DSP_SEQUENCE()`
255/// @details
256/// This move-only builder accumulates DSP units via chained `>>` operators and
257/// converts them into a `DSPSequence` via `build()`. Not intended to be used directly,
258/// unless implementing advanced custom sequence-building logic.
259/// @ingroup DSP
260/// @private
261template <typename SampleType>
262class DSPSequenceBuilder
263{
264public:
265 DSPSequenceBuilder() = default;
266 DSPSequenceBuilder (const DSPSequenceBuilder&) = delete;
267 DSPSequenceBuilder (DSPSequenceBuilder&&) noexcept = default;
268 DSPSequenceBuilder& operator= (const DSPSequenceBuilder&) = delete;
269 DSPSequenceBuilder& operator= (DSPSequenceBuilder&&) noexcept = default;
270 ~DSPSequenceBuilder() = default;
271
272 template <typename DerivedDSP>
273 DSPSequenceBuilder&& operator>> (DerivedDSP&& dsp) &&
274 {
275 m_dspChain.emplace_back (dsp.move());
276 return std::move (*this);
277 }
278
279 DSPSequenceBuilder&& operator>> (std::unique_ptr<DSPBase<SampleType>> dsp) &&
280 {
281 m_dspChain.emplace_back (std::move (dsp));
282 return std::move (*this);
283 }
284
285 DSPSequence<SampleType> build()
286 {
287 return DSPSequence<SampleType> (std::move (m_dspChain));
288 }
289
290private:
291 std::vector<std::unique_ptr<DSPBase<SampleType>>> m_dspChain;
292};
293
295HART_DSP_DECLARE_ALIASES_FOR (DSPSequenceBuilder);
296
297/// @brief Creates a linear DSP sequence using concise chain syntax
298/// @details
299/// This macro is the preferred way to create a `DSPSequence`.
300///
301/// It accepts a chain of DSP units chained with `>>`, preserving the exact
302/// order in which they should process the audio.
303///
304/// Example:
305/// @code
306/// auto myDspChain = HART_DSP_SEQUENCE (
307/// GainDb (-3_dB) >> HardClip (-1_dB) >> TimeShift (5_ms)
308/// );
309/// @endcode
310///
311/// The resulting object behaves as a normal DSP subclass and can, among other
312/// things, be:
313/// - passed into `processAudioWith()`
314/// - used in a `Signal`'s DSP chain
315/// - nested into another sequence
316/// - modified by appending or popping DSP elements
317/// - deep-copied via `copy()`
318///
319/// @note
320/// This macro depends on HART DSP aliases being declared for the chosen sample
321/// type, for example:
322/// @code
323/// HART_DECLARE_ALIASES_FOR_FLOAT;
324/// @endcode
325/// (or a similar macro for `double`).
326/// You can also just use explicitly typed`HART_DSP_SEQUENCE_T()`.
327/// @ingroup DSP
328#define HART_DSP_SEQUENCE(...)
329 (DSPSequenceBuilder() >> __VA_ARGS__).build()
330
331/// @brief Creates a DSP sequence for an explicit sample type
332/// @details
333/// This typed variant of `HART_DSP_SEQUENCE(...)` is useful when DSP aliases
334/// are not declared.
335///
336/// Example:
337/// @code
338/// auto chain = HART_DSP_SEQUENCE_T (
339/// float,
340/// GainDb<float> (-3_dB) >> HardClip<float> (-1_dB)
341/// );
342/// @endcode
343///
344/// @param SampleType The sample type used by the sequence and all DSP units
345/// @ingroup DSP
346#define HART_DSP_SEQUENCE_T(SampleType, ...)
347 (hart::DSPSequenceBuilder<SampleType>() >> __VA_ARGS__).build()
348
349} // namespace hart
Container for audio data.
A set of boolean flags mapped to each audio channel.
Polymorphic base for all DSP.
Definition hart_dsp.hpp:33
A DSP unit that renders audio through a linear sequence of DSP instances.
DSPSequence & operator=(DSPSequence &&) noexcept=default
Moves DSP chain from another DSP sequence.
bool supportsSampleRate(double sampleRate) const override
Tells whether this effect supports given sample rate.
virtual void setValue(int, double) override
Sets DSP value.
size_t size() const noexcept
Returns the number of DSP elements in the sequence.
DSPSequence(std::vector< std::unique_ptr< DSPBase< SampleType > > > dspChain)
Creates a sequence of DSP processors.
~DSPSequence() override=default
Destructor.
void process(const AudioBuffer< SampleType > &input, AudioBuffer< SampleType > &output, const EnvelopeBuffers &, ChannelFlags) override
Processes the audio.
std::unique_ptr< DSPBase< SampleType > > copy() const override
Returns a smart pointer with a copy of this object.
void represent(std::ostream &stream) const override
Makes a text representation of this DSP effect for test failure outputs.
const DSPBase< SampleType > * operator[](long long int index) const
Accesses a specific element in the DSP sequence (immutable version)
DSPBase< SampleType > * operator[](long long int index)
Accesses a specific element in the DSP sequence (mutable version)
DSPSequence(DSPSequence &&) noexcept=default
Moves DSP chain from another DSP sequence.
void prepare(double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames) override
Prepare for processing.
virtual bool supportsEnvelopeFor(int) const override
Tells whether this effect accepts automation envelopes for a particular parameter.
std::unique_ptr< DSPBase< SampleType > > pop(long long int index=-1)
Extracts a specific element in the DSP chain by removing it.
DSPSequence(const DSPSequence &)=delete
No copy construction.
void append(DerivedDSP &&dsp)
Appends an element to the end of DSP chain.
DSPSequence & operator=(const DSPSequence &)=delete
No copy assignment.
bool supportsChannelLayout(size_t numInputChannels, size_t numOutputChannels) const override
Tells the runner (host) whether this effect supports a specific i/o configuration.
void append(std::unique_ptr< DSPBase< SampleType > > dsp)
Appends an element to the end of DSP chain.
Base for DSP effects.
Definition hart_dsp.hpp:345
Thrown when a container index is out of range.
Thrown when a nullptr could be handled gracefully.
#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_THROW_OR_RETURN(ExceptionType, message, returnValue)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns a specified ...
#define HART_DSP_DECLARE_ALIASES_FOR(ClassName)
Definition hart_dsp.hpp:619