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