HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_signal_function.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <functional>
5#include <memory>
6
7#include "signals/hart_signal.hpp"
8#include "hart_utils.hpp" // floatsEqual(), Loop
9
10// TODO: Allow user buffer mismatch, if it's mono, and do multiplexing?
11
12namespace hart
13{
14
15/// @brief Signal defined by a user-provided function
16/// @ingroup Signals
17///
18/// @details
19/// This Signal allows defining input signals using a lambda or function object,
20/// instead of creating a dedicated Signal subclass.
21///
22/// The provided function is called during @ref prepare() and is expected to fill an
23/// initially empty @ref AudioBuffer with audio data. This buffer is then used as a
24/// source and streamed block-by-block during processing.
25///
26/// The generated buffer can either:
27/// - loop continuously (SignalFunction::Loop::yes), or
28/// - play once and output silence afterwards (SignalFunction::Loop::no)
29///
30/// `SignalFunction` can be suitable for:
31/// - Procedural signal generation (e.g. waveforms, noise)
32/// - Defining short repeating patterns (e.g. one-cycle signals)
33/// - Creating custom test signals inline in test cases
34///
35/// @par Example
36/// @code
37// auto nyquistSignal = SignalFunction (
38/// [] (AudioBuffer& buffer)
39/// {
40/// buffer.setNumFrames (2);
41/// for (size_t channel = 0; channel < buffer.getNumChannels(); ++channel)
42/// {
43/// buffer[channel][0] = -1.0f;
44/// buffer[channel][0] = 1.0f;
45/// }
46/// },
47/// "Nyquist Signal",
48/// SignalFunction::Loop::yes
49/// );
50/// @endcode
51///
52/// @throws hart::NullPointerError if `signalFunction` is empty
53/// @throws hart::ChannelLayoutError if the function changes buffer's channel count
54/// @throws hart::SampleRateError if the function changes buffer's sample rate
55/// @throws hart::SizeError if the function does not allocate any frames
56///
57/// @ingroup Signals
58template<typename SampleType>
60 public Signal<SampleType, SignalFunction<SampleType>>
61{
62public:
63 /// @brief Constructs a signal from a user-defined function.
64 /// @details
65 /// The provided function is guaranteed to be called with an empty
66 /// @ref AudioBuffer, i.e. AudioBuffer::getNumFrames() will return zero.
67 /// The function must allocate and fill the buffer with audio data.
68 /// The provided buffer is guaranteed to contain valid metadata on required
69 /// number of channels (see @ref AudioBuffer::getNumChannels()) and
70 /// sample rate (see @ref AudioBuffer::getSampleRateHz()).
71 ///
72 /// The buffer is owned internally and reused during processing.
73 ///
74 /// @param signalFunction A callable with signature:
75 /// `void (AudioBuffer<SampleType>&)`
76 ///
77 /// For the provided buffer, the function must:
78 /// - Allocate number of frames necessary for your Signal
79 /// - Fill the buffer with valid audio data
80 /// - Preserve the number of channels and sample rate
81 /// Failure to meet these requirements will result in a runtime error.
82 ///
83 /// @param label A human-readable description of the signal, used in logs
84 /// and failure reports. Can be empty.
85 ///
86 /// @param loop If set to Loop::yes, the generated buffer will repeat
87 /// continuously. If set to Loop::no, the signal will output silence
88 /// (zeros) after the buffer is exhausted.
89 SignalFunction (std::function <void (AudioBuffer<SampleType>&)> signalFunction, const std::string& label = {}, Loop loop = Loop::yes) :
90 m_signalFunction (std::move (signalFunction)),
91 m_label (label),
92 m_loop (loop)
93 {
94 }
95
96 void renderNextBlock (AudioBuffer<SampleType>& output) override
97 {
98 // Sanity check
99 hassert (m_userBuffer != nullptr);
100
101 // User is not alowed to sabbotage with their buffer in any way
102 hassert (m_userBuffer->getNumChannels() == output.getNumChannels());
103 hassert (m_userBuffer->getNumFrames() != 0);
104 hassert (
105 m_userBuffer->hasSampleRate()
106 && output.hasSampleRate()
107 && floatsEqual (output.getSampleRateHz(), m_userBuffer->getSampleRateHz())
108 );
109
110 const size_t bufferFrames = m_userBuffer->getNumFrames();
111 const size_t numFrames = output.getNumFrames();
112 const size_t numChannels = output.getNumChannels();
113
114 size_t readPos = m_userBufferOffsetFrames;
115 size_t outputOffsetFrames = 0;
116
117 while (outputOffsetFrames < numFrames)
118 {
119 const size_t framesAvailable = bufferFrames - readPos;
120 const size_t framesToCopy = std::min (framesAvailable, numFrames - outputOffsetFrames);
121
122 for (size_t channel = 0; channel < numChannels; ++channel)
123 {
124 std::copy (
125 (*m_userBuffer)[channel] + readPos,
126 (*m_userBuffer)[channel] + readPos + framesToCopy,
127 output[channel] + outputOffsetFrames
128 );
129 }
130
131 outputOffsetFrames += framesToCopy;
132 readPos += framesToCopy;
133
134 if (readPos >= bufferFrames)
135 {
136 if (m_loop == Loop::yes)
137 readPos = 0;
138 else
139 break;
140 }
141 }
142
143 while (outputOffsetFrames < numFrames)
144 {
145 for (size_t channel = 0; channel < numChannels; ++channel)
146 output[channel][outputOffsetFrames] = static_cast<SampleType> (0);
147
148 ++outputOffsetFrames;
149 }
150
151 m_userBufferOffsetFrames = readPos;
152 }
153
154 void prepare (double sampleRateHz, size_t numOutputChannels, size_t /*maxBlockSizeFrames*/) override
155 {
156 // If internal buffer has some audio in it, we expect it to have a certain sample rate
157 if (m_userBuffer != nullptr)
158 hassert (m_userBuffer->hasSampleRate());
159
160 const bool bufferDoesntNeedGenerating =
161 m_userBuffer != nullptr
162 && m_userBuffer->getNumFrames() > 0
163 && numOutputChannels == m_userBuffer->getNumChannels()
164 && floatsEqual (sampleRateHz, m_userBuffer->getSampleRateHz());
165
166 if (bufferDoesntNeedGenerating)
167 return;
168
169 m_userBuffer = std::make_shared<AudioBuffer<SampleType>> (numOutputChannels, 0, sampleRateHz);
170
171 if (m_signalFunction == nullptr)
172 HART_THROW_OR_RETURN_VOID (hart::NullPointerError, "Signal function is a nullptr!");
173
174 hassert (m_userBuffer->getNumFrames() == 0); // Guaranteed to provide an empty buffer
175 m_signalFunction (*m_userBuffer);
176
177 if (m_userBuffer->getNumChannels() != numOutputChannels)
178 HART_THROW_OR_RETURN_VOID (hart::ChannelLayoutError, "Your signal function shouldn't alter the number of buffer's channels");
179
180 if (! m_userBuffer->hasSampleRate() || ! floatsEqual (sampleRateHz, m_userBuffer->getSampleRateHz()))
181 HART_THROW_OR_RETURN_VOID (hart::SampleRateError, "Your signal function shouldn't alter the buffer's sample rate");
182
183 if (m_userBuffer->getNumFrames() == 0)
184 HART_THROW_OR_RETURN_VOID (hart::SizeError, "Your signal function should allocate at least one frame in the buffer");
185 }
186
187 void reset() override
188 {
189 m_userBufferOffsetFrames = 0;
190 }
191
192 void represent (std::ostream& stream) const override
193 {
194 stream << "SignalFunction (<function>, \"" << m_label << (m_loop == Loop::yes ? "\", Loop::yes)" : "\", Loop::no)");
195 }
196
197private:
198 const std::function <void (AudioBuffer<SampleType>&)> m_signalFunction = nullptr;
199 const std::string m_label;
200 const Loop m_loop;
201 std::shared_ptr<AudioBuffer<float>> m_userBuffer;
202 size_t m_userBufferOffsetFrames = 0;
203};
204
206
207} // namespace hart
Container for audio data.
SampleType * operator[](size_t channel)
Get a raw pointer to a specific channel's mutable audio data.
size_t getNumFrames() const
Get number of frames (samples)
bool hasSampleRate() const
Check if a specific sample rate was assigned to the audio buffer.
double getSampleRateHz() const
Get a sample rate metadata.
size_t getNumChannels() const
Get number of channels.
Thrown when a numbers of channels is mismatched.
Thrown when a nullptr could be handled gracefully.
Thrown when sample rate is mismatched.
Signal defined by a user-provided function.
void renderNextBlock(AudioBuffer< SampleType > &output) override
Renders next block audio for the signal.
void prepare(double sampleRateHz, size_t numOutputChannels, size_t) override
Prepare the signal for rendering.
void represent(std::ostream &stream) const override
Makes a text representation of this Signal for test failure outputs.
SignalFunction(std::function< void(AudioBuffer< SampleType > &)> signalFunction, const std::string &label={}, Loop loop=Loop::yes)
Constructs a signal from a user-defined function.
void reset() override
Resets the Signal to initial state.
Base class for signals.
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
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.
Loop
Helper values for something that could loop, like a Signal.
#define HART_SIGNAL_DECLARE_ALIASES_FOR(ClassName)