HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_dsp_function.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <functional>
4
5#include "hart_dsp.hpp"
7
8namespace hart
9{
10
11/// @brief A DSP processor defined by a user-provided function
12///
13/// @details
14/// It allows defining audio processing logic using a lambda or
15/// function object, without defining a dedicated DSP subclass.
16///
17/// Three function styles are supported:
18///
19/// @par 1. Sample-wise processing
20/// @code
21/// SampleType (SampleType value)
22/// @endcode
23/// Processes each sample independently.
24///
25/// @par 2. Block-wise replacing (in-place) processing
26/// @code
27/// void (AudioBuffer<SampleType>& buffer)
28/// @endcode
29/// Processes the buffer in-place. The buffer is pre-filled with input data.
30///
31/// @par 3. Block-wise non-replacing processing
32/// @code
33/// void (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output)
34/// @endcode
35/// Provides separate input and output buffers.
36///
37/// @par Buffer invariants
38/// For mutable buffers in block-wise replacing and non-replacing modes:
39/// - Number of frames must not be changed
40/// - Number of channels must not be changed
41/// - Sample rate must not be changed
42///
43/// Disobeying those constraints will result in a runtime error.
44///
45/// @par Example
46/// @code
47/// auto myDsp = DSPFunction<float> (
48/// [] (float x) { return x * 0.5f; },
49/// "Gain"
50/// );
51/// @endcode
52///
53/// @note This class is primarily intended for simple and expressive DSP
54/// definitions in tests. For more complex or stateful processors, it's
55/// encouraged to implement a dedicated DSP subclass.
56/// @ingroup DSP
57template<typename SampleType>
59 public DSP<SampleType, DSPFunction<SampleType>>
60{
61public:
62 /// @brief Creates a DSP instance based on a sample-wise DSP function
63 /// @note If you need to be aware of sample rate value, or what channel
64 /// a given sample is from, use block-wise function signature instead
65 /// (see other ctors of this class for details). Or define a full-featured
66 /// DSP subclass for your DSP, instead of a simple function-based one.
67 /// @details
68 /// This is the simplest form of a custom DSP. The function is applied
69 /// independently to each sample in the input signal.
70 ///
71 /// @par Example
72 /// @code
73 /// auto myDsp = DSPFunction<float> (
74 /// [] (float x) { return x * 0.5f; },
75 /// "Gain"
76 /// );
77 /// @endcode
78 /// @param dspFunction Function with signature:
79 /// @code
80 /// SampleType (SampleType value)
81 /// @endcode
82 /// Will be called to process each sample, expected to return a processed value.
83 /// @param label Optional human-readable label to use in test error reporting.
84 DSPFunction (std::function<SampleType (SampleType)> dspFunction, const std::string& label = {}):
85 m_dspFunctionSampleWise (std::move (dspFunction)), m_label (label)
86 {
87 }
88
89 /// @brief Creates a DSP instance based on a block-wise replacing (in-place) DSP function.
90 /// @details The provided function will be called for each block of input audio.
91 /// It will receive a buffer pre-filled with input audio data. The function is expected
92 /// to modify the buffer directly. The provided buffer is guaranteed to have:
93 /// - A number of channels equal to `max (inputChannels, outputChannels)`
94 /// - Input channels populated with input data
95 /// - Additional channels (if any) initialized to zero
96 /// - Number of frames equal to the block size specified in the test case,
97 /// or less than that in case of partial blocks.
98 /// - Correct sample rate value in its metadata (see AudioBuffer::getSampleRateHz())
99 ///
100 /// For the provided buffer, the function must not change:
101 /// - Number of channels
102 /// - Number of frames
103 /// - Sample rate
104 ///
105 /// Disobeying those constraints will result in a runtime error.
106 /// @par Example
107 /// @code
108 /// auto myDsp = DSPFunction<float> (
109 /// [] (auto& buffer)
110 /// {
111 /// // Process buffer in-place
112 /// },
113 /// "My DSP function"
114 /// );
115 /// @endcode
116 /// @param dspFunction Function with signature:
117 /// @code
118 /// void (AudioBuffer<SampleType>& buffer)
119 /// @endcode
120 /// Expected to read the input samples in the buffer and replace them with processed data.
121 /// @param label Optional human-readable label to use in test error reporting.
122 DSPFunction (std::function<void (AudioBuffer<SampleType>&)> dspFunction, const std::string& label = {}):
123 m_dspFunctionBlockWiseReplacing (std::move (dspFunction)), m_label (label)
124 {
125 }
126
127 /// @brief Creates a DSP instance based on a block-wise non-replacing (separate input and output buffers) DSP function.
128 /// @details
129 /// This form provides full control over how output is generated from input.
130 /// The output buffer is pre-allocated with the expected size.
131 ///
132 /// For the output buffer, the function must not change:
133 /// - Number of channels
134 /// - Number of frames
135 /// - Sample rate
136 ///
137 /// Disobeying those constraints will result in a runtime error.
138 ///
139 /// @par Example
140 /// @code
141 /// auto myDsp = DSPFunction<float> (
142 /// [] (const auto& in, auto& out)
143 /// {
144 /// // Fill output using input
145 /// },
146 /// "My DSP function"
147 /// );
148 /// @endcode
149 /// @param dspFunction Function with signature:
150 /// @code
151 /// void (const AudioBuffer<SampleType>& input,
152 /// AudioBuffer<SampleType>& output)
153 /// @endcode
154 /// Expected to fill the output buffer with processed data from the input buffer.
155 /// @param label Optional human-readable label to use in test error reporting.
156 DSPFunction (std::function<void (const AudioBuffer<SampleType>&, AudioBuffer<SampleType>&)> dspFunction, const std::string& label = {}):
157 m_dspFunctionBlockWiseNonReplacing (std::move (dspFunction)), m_label (label)
158 {
159 }
160
161 void prepare (double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames) override
162 {
163 // Make sure you've passed one of the correct function signatures to the DSP ctor!
164 hassert (
165 m_dspFunctionBlockWiseNonReplacing != nullptr
166 || m_dspFunctionBlockWiseReplacing != nullptr
167 || m_dspFunctionSampleWise != nullptr);
168
169 if (m_dspFunctionBlockWiseReplacing != nullptr)
170 {
171 const size_t maxChannels = std::max (numInputChannels, numOutputChannels);
172 m_inputOutputBuffer = AudioBuffer<SampleType> (maxChannels, maxBlockSizeFrames, sampleRateHz);
173 }
174 }
175
176 void process (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output, const EnvelopeBuffers& /* envelopeBuffers */, ChannelFlags /* channelsToProcess*/ ) override
177 {
178 if (m_dspFunctionBlockWiseNonReplacing != nullptr)
179 {
180 processBlockWiseNonReplacing (input, output);
181 return;
182 }
183
184 if (m_dspFunctionBlockWiseReplacing != nullptr)
185 {
186 processBlockWiseReplacing (input, output);
187 return;
188 }
189
190 if (m_dspFunctionSampleWise != nullptr)
191 {
192 processSampleWise (input, output);
193 return;
194 }
195
196 HART_THROW_OR_RETURN_VOID (hart::NullPointerError, "DSP function is nullptr");
197 }
198
199 void represent (std::ostream& stream) const override
200 {
201 stream << "DSPFunction (<function>, \"" << m_label << "\")";
202 }
203
204 void reset() override {}
205 void setValue (int /*paramId*/, double /*value*/) override {}
206 bool supportsSampleRate (double /*sampleRateHz*/) const override { return true; }
207 bool supportsEnvelopeFor (int /*paramId*/) const override { return false; }
208
209 bool supportsChannelLayout (size_t numInputChannels, size_t numOutputChannels) const override
210 {
211 // There's no graceful way to handle non-matching channel numbers with sample-wise
212 // function (other than perhaps mono-to-many), hence refusing to render those.
213 if (m_dspFunctionSampleWise != nullptr)
214 return numInputChannels == numOutputChannels;
215
216 return true;
217 }
218
219private:
220 void processSampleWise (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output)
221 {
222 // supportsChannelLayout() should reject non-matching channel numbers for sample-wise function
223 hassert (input.getNumChannels() == output.getNumChannels());
224
225 const size_t numChannels = input.getNumChannels();
226 const size_t numFrames = input.getNumFrames();
227
228 for (size_t channel = 0; channel < numChannels; ++channel)
229 for (size_t i = 0; i < numFrames; ++i)
230 output[channel][i] = m_dspFunctionSampleWise (input[channel][i]);
231 }
232
233 void processBlockWiseReplacing (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output)
234 {
235 const size_t numFrames = input.getNumFrames();
236
237 if (m_inputOutputBuffer.getNumFrames() != numFrames)
238 m_inputOutputBuffer.setNumFrames (numFrames); // Assuming it's a switch to a partial block, or back from it
239
240 for (size_t channel = 0; channel < input.getNumChannels(); ++channel)
241 m_inputOutputBuffer.copyFrom (channel, 0, input, channel, 0, numFrames);
242
243 for (size_t channel = input.getNumChannels(); channel < m_inputOutputBuffer.getNumChannels(); ++channel)
244 m_inputOutputBuffer.clear (channel, 0, numFrames);
245
246 const size_t originalNumChannels = m_inputOutputBuffer.getNumChannels();
247 const size_t originalNumFrames = m_inputOutputBuffer.getNumFrames();
248 const double originalSampleRateHz = m_inputOutputBuffer.getSampleRateHz();
249
250 m_dspFunctionBlockWiseReplacing (m_inputOutputBuffer);
251
252 if (m_inputOutputBuffer.getNumChannels() != originalNumChannels)
253 HART_THROW_OR_RETURN_VOID (hart::ChannelLayoutError, "In-place DSP must not change the number of channels in the buffer");
254
255 if (m_inputOutputBuffer.getNumFrames() != originalNumFrames)
256 HART_THROW_OR_RETURN_VOID (hart::SizeError, "In-place DSP must not change the buffer length");
257
258 if (!floatsEqual (m_inputOutputBuffer.getSampleRateHz(), originalSampleRateHz))
259 HART_THROW_OR_RETURN_VOID (hart::SampleRateError, "In-place DSP must not change buffer's sample rate");
260
261 for (size_t channel = 0; channel < output.getNumChannels(); ++channel)
262 output.copyFrom (channel, 0, m_inputOutputBuffer, channel, 0, numFrames);
263 }
264
265 void processBlockWiseNonReplacing (const AudioBuffer<SampleType>& input, AudioBuffer<SampleType>& output)
266 {
267 const size_t originalNumChannels = output.getNumChannels();
268 const size_t originalNumFrames = output.getNumFrames();
269 const double originalSampleRateHz = output.getSampleRateHz();
270
271 m_dspFunctionBlockWiseNonReplacing (input, output);
272
273 // User's function is only allowed to modify the actual frames, and nothing else
274
275 if (output.getNumChannels() != originalNumChannels)
276 HART_THROW_OR_RETURN_VOID (hart::ChannelLayoutError, "DSP must not change number of channels in the output buffer");
277
278 if (output.getNumFrames() != originalNumFrames)
279 HART_THROW_OR_RETURN_VOID (hart::SizeError, "DSP must not change the output buffer length");
280
281 if (! floatsEqual (output.getSampleRateHz(), originalSampleRateHz))
282 HART_THROW_OR_RETURN_VOID (hart::SampleRateError, "DSP must not change sample rate of the output buffer");
283 }
284
285private:
286 std::function<SampleType (SampleType)> m_dspFunctionSampleWise = nullptr;
287 std::function<void (AudioBuffer<SampleType>&)> m_dspFunctionBlockWiseReplacing = nullptr;
288 std::function<void (const AudioBuffer<SampleType>&, AudioBuffer<SampleType>&)> m_dspFunctionBlockWiseNonReplacing = nullptr;
289 std::string m_label;
290 AudioBuffer<SampleType> m_inputOutputBuffer;
291};
292
294
295} // namespace hart
Container for audio data.
A set of boolean flags mapped to each audio channel.
Thrown when a numbers of channels is mismatched.
A DSP processor defined by a user-provided function.
DSPFunction(std::function< void(AudioBuffer< SampleType > &)> dspFunction, const std::string &label={})
Creates a DSP instance based on a block-wise replacing (in-place) DSP function.
DSPFunction(std::function< void(const AudioBuffer< SampleType > &, AudioBuffer< SampleType > &)> dspFunction, const std::string &label={})
Creates a DSP instance based on a block-wise non-replacing (separate input and output buffers) DSP fu...
void setValue(int, double) override
Sets DSP value.
void process(const AudioBuffer< SampleType > &input, AudioBuffer< SampleType > &output, const EnvelopeBuffers &, ChannelFlags) override
Processes the audio.
void represent(std::ostream &stream) const override
Makes a text representation of this DSP effect for test failure outputs.
void prepare(double sampleRateHz, size_t numInputChannels, size_t numOutputChannels, size_t maxBlockSizeFrames) override
Prepare for processing.
bool supportsEnvelopeFor(int) const override
Tells whether this effect accepts automation envelopes for a particular parameter.
bool supportsSampleRate(double) const override
Tells whether this effect supports given sample rate.
DSPFunction(std::function< SampleType(SampleType)> dspFunction, const std::string &label={})
Creates a DSP instance based on a sample-wise DSP function.
void reset() override
Resets to initial state.
bool supportsChannelLayout(size_t numInputChannels, size_t numOutputChannels) const override
Tells the runner (host) whether this effect supports a specific i/o configuration.
Base for DSP effects.
Definition hart_dsp.hpp:345
Thrown when a nullptr could be handled gracefully.
Thrown when sample rate is mismatched.
Thrown when an unexpected container size 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 hassert(condition)
Triggers a HartAssertException if the condition is false
#define HART_DSP_DECLARE_ALIASES_FOR(ClassName)
Definition hart_dsp.hpp:619