HART  0.2.0
High level Audio Regression and Testing
Loading...
Searching...
No Matches
hart_audio_buffer.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm> // max_element(), copy(), fill()
4#include <cmath> // isnan()
5#include <vector>
6
8#include "hart_precision.hpp" // hzPrecision
9#include "hart_utils.hpp" // nan(), floatsEqual()
10
11/// @defgroup DataStructures Data Structures
12/// @brief Custom data structures and containers
13
14namespace hart {
15
16/// @brief Container for audio data
17/// @note This class owns a memory block with audio samples - so it's a container, not a view. Treat it like a heavyweight object.
18/// @ingroup DataStructures
19template <typename SampleType>
21{
22public:
23 /// @brief Creates an audio buffer
24 /// @param numChannels Initial number of channels
25 /// @param numFrames Numbers of frames (samples) to be allocated in each channel
26 /// @param sampleRateHz Metadata for sample rate (in Hz) in which the data whould be interpreted, whenever applicable
27 AudioBuffer (size_t numChannels = 0, size_t numFrames = 0, double sampleRateHz = nan<double>()) :
28 m_numChannels (numChannels),
29 m_numFrames (numFrames),
30 m_sampleRateHz (sampleRateHz),
31 m_frames (m_numChannels * m_numFrames),
32 m_channelPointers (m_numChannels)
33 {
34 updateChannelPointers();
35 }
36
37 /// @brief Creates an audio buffer by copying
38 AudioBuffer (const AudioBuffer& other) :
39 m_numChannels (other.m_numChannels),
40 m_numFrames (other.m_numFrames),
41 m_sampleRateHz (other.m_sampleRateHz),
42 m_frames (other.m_frames),
43 m_channelPointers (m_numChannels)
44 {
45 updateChannelPointers();
46 }
47
48 /// @brief Creates an audio buffer by moving
50 m_numChannels (other.m_numChannels),
51 m_numFrames (other.m_numFrames),
52 m_sampleRateHz (other.m_sampleRateHz),
53 m_frames (std::move (other.m_frames)),
54 m_channelPointers (std::move (other.m_channelPointers))
55 {
56 other.erase();
57 }
58
59 /// @brief The destructor
60 ~AudioBuffer() = default;
61
62 /// @brief Creates an audio buffer by copy-assigning
64 {
65 if (this == &other)
66 return *this;
67
68 if (m_numChannels != other.m_numChannels)
69 HART_THROW_OR_RETURN (hart::ChannelLayoutError, "Can't copy from a buffer with different number of channels", *this);
70
71 m_numFrames = other.m_numFrames;
72 m_sampleRateHz = other.m_sampleRateHz;
73 m_frames = other.m_frames;
74 m_channelPointers.resize (m_numChannels);
75 updateChannelPointers();
76
77 return *this;
78 }
79
80 /// @brief Creates an audio buffer by move-assigning
82 {
83 if (this == &other)
84 return *this;
85
86 m_numChannels = other.m_numChannels;
87 m_numFrames = other.m_numFrames;
88 m_sampleRateHz = other.m_sampleRateHz;
89 m_frames = std::move (other.m_frames);
90 m_channelPointers = std::move (other.m_channelPointers);
91 other.erase();
92
93 return *this;
94 }
95
96 /// @brief Gets a raw pointer to the read-only audio data
97 /// @return A pointer to the beginning of the audio data. Guaranteed to be a contiguous non-interleaved block of memory.
98 const SampleType* const* getArrayOfReadPointers() const
99 {
100 return static_cast<const SampleType* const*> (m_channelPointers.data());
101 }
102
103 /// @brief Gets a raw pointer to the mutable audio data
104 /// @return A pointer to the beginning of the audio data. Guaranteed to be a contiguous non-interleaved block of memory.
105 SampleType* const* getArrayOfWritePointers()
106 {
107 return m_channelPointers.data();
108 }
109
110 /// @brief Creates an empty audio buffer with the same number of channels, frames and sample rate as the `other` buffer.
111 /// @note The data from other buffer will not be copied, expect un-initialized values.
112 /// @param other Reference buffer
113 static AudioBuffer emptyLike (const AudioBuffer& other)
114 {
115 return other.hasSampleRate()
116 ? AudioBuffer (other.getNumChannels(), other.getNumFrames(), other.getSampleRateHz())
117 : AudioBuffer (other.getNumChannels(), other.getNumFrames());
118 }
119
120 /// @brief Get number of channels
121 /// @return Number of allocated channels
122 size_t getNumChannels() const { return m_numChannels; }
123
124 /// @brief Get number of frames (samples)
125 /// @return Number of allocated frames (samples) in every channel
126 size_t getNumFrames() const { return m_numFrames; }
127
128 /// @brief Resizes the buffer to hold a new number of frames per channel
129 /// @details Resizing behaviour:
130 /// - If newNumFrames == current size, it won't do anything.
131 /// - If newNumFrames > current size, it will append silence (zeros) at the end.
132 /// - If newNumFrames < current size: it will jsut truncate, discarding samples from the end.
133 /// Will always preserve existing channels and sample-rate metadata.
134 /// @attention May cause reallocation, thus invalidating previous raw pointers!
135 /// @param newNumFrames New number of frames per channel
136 void setNumFrames (size_t newNumFrames)
137 {
138 if (newNumFrames == m_numFrames)
139 return;
140
141 const size_t oldTotalSamples = m_frames.size();
142 const size_t newTotalSamples = m_numChannels * newNumFrames;
143
144 if (newNumFrames < m_numFrames)
145 {
146 m_frames.resize (newTotalSamples);
147 m_numFrames = newNumFrames;
148 updateChannelPointers();
149 return;
150 }
151
152 if (newTotalSamples > oldTotalSamples)
153 {
154 m_frames.resize (newTotalSamples);
155 std::fill (
156 m_frames.begin() + oldTotalSamples,
157 m_frames.end(),
158 SampleType (0)
159 );
160 }
161
162 m_numFrames = newNumFrames;
163 updateChannelPointers();
164 }
165
166 /// @brief Check if a specific sample rate was assigned to the audio buffer
167 /// @note Unititialized buffers, as well as some specific Signal types, will not have a specific sample rate.
168 /// @return `true` if there is a specific sample rate value, `false` otherwise
169 bool hasSampleRate() const
170 {
171 return ! std::isnan (m_sampleRateHz);
172 }
173
174 /// @brief Get a sample rate metadata
175 /// @note Call `hasSampleRate()` if you're not sure if the buffer has a specific sample rate
176 /// @return Buffer's sample rate in Hz
177 double getSampleRateHz() const
178 {
179 hassert (! std::isnan (m_sampleRateHz)); // Call hasSampleRate() first! This buffer doesn't have a specific sample rate.
180 return m_sampleRateHz;
181 }
182
183 /// @brief Get a duration of the audio buffer
184 /// @note Before calling it, it's a good idea to call `hasSampleRate()` to check if the buffer has a specific sample rate, otherwise the duration is unknown.
185 /// @return Duration of the audio buffer in seconds
186 double getLengthSeconds() const
187 {
188 if (m_numFrames == 0)
189 return 0.0;
190
191 hassert (! std::isnan (m_sampleRateHz)); // This buffer doesn't have a specific sample rate, and hence the length is unknown
192 hassert (! floatsEqual (m_sampleRateHz, 0.0));
193 return static_cast <double> (m_numFrames) / m_sampleRateHz;
194 }
195
196 /// @brief Get a raw pointer to a specific channel's mutable audio data
197 /// @note The data is guaranteed to have at least getNumFrames() amount of items, and guaranteed to be contiguus and non-interleaves block of memory
198 /// @return Pointer to the audio data
199 SampleType* operator[] (size_t channel)
200 {
201 return m_channelPointers[channel];
202 }
203
204 /// @brief Get a raw pointer to a specific channel's read-only audio data
205 /// @note The data is guaranteed to have at least getNumFrames() amount of items, and guaranteed to be contiguus and non-interleaves block of memory
206 /// @return Pointer to the audio data
207 const SampleType* operator[] (size_t channel) const
208 {
209 return m_channelPointers[channel];
210 }
211
212 /// @brief Appends data from another buffer
213 /// @warning This operation will resize the current buffer and potentially invalidate the previous raw data pointers returned by
214 /// `getArrayOfReadPointers()`, `getArrayOfWritePointers()` and the "`[]`" operator, so make sure to keep those external pointers up to date.
215 /// @param otherBuffer A buffer to append from
216 void appendFrom (const AudioBuffer<SampleType>& otherBuffer)
217 {
218 if (otherBuffer.getNumChannels() != m_numChannels)
219 HART_THROW_OR_RETURN_VOID (hart::ChannelLayoutError, "Channel count mismatch");
220
221 // Not a huge deal, but appending a buffer with a different SR must be at least a little suspicious
222 hassert ((! hasSampleRate() || ! otherBuffer.hasSampleRate()) || floatsEqual (getSampleRateHz(), otherBuffer.getSampleRateHz()));
223
224 const size_t thisNumFrames = m_numFrames;
225 const size_t otherNumFrames = otherBuffer.getNumFrames();
226
227 std::vector<SampleType> combinedFrames (m_numChannels * (thisNumFrames + otherNumFrames));
228
229 for (size_t channel = 0; channel < m_numChannels; ++channel)
230 {
231 SampleType* newChannelStart = &combinedFrames[channel * (thisNumFrames + otherNumFrames)];
232 std::copy (m_channelPointers[channel], m_channelPointers[channel] + thisNumFrames, newChannelStart);
233 std::copy (otherBuffer[channel], otherBuffer[channel] + otherNumFrames, newChannelStart + thisNumFrames);
234 }
235
236 m_frames = std::move (combinedFrames);
237 m_numFrames += otherNumFrames;
238
239 updateChannelPointers();
240 }
241
242 /// @brief Clears the buffer
243 /// @details The number of frames after this operaion will be zero, but channel number will persist.
244 void erase()
245 {
246 // Keeping the sample rate though, just wiping the data
247
248 m_numFrames = 0;
249 m_frames.clear();
250
251 // If m_channelPointers was std::move'd, its size will be zero
252 if (m_channelPointers.size() != m_numChannels)
253 m_channelPointers.resize (m_numChannels);
254
255 updateChannelPointers();
256 }
257
258 /// @brief Get the maximum absolute value in the buffer in a specific channel
259 /// @param channel Channel of which the magnitude should be measured
260 /// @param startFrame The beginnig of the range of the frames to look for magnitude
261 /// @param numFrames Number of frames, beginning with startFrame, to look for the magnitude
262 /// @return The maximum value, in linear domain (i.e. not decibels)
263 SampleType getMagnitude (size_t channel, size_t startFrame, size_t numFrames) const
264 {
265 if (channel >= m_numChannels)
266 HART_THROW_OR_RETURN (hart::IndexError, "Invalid channel", (SampleType) 0);
267
268 if (startFrame + numFrames > m_numFrames || numFrames == 0)
269 HART_THROW_OR_RETURN (hart::IndexError, "Invalid frame range", (SampleType) 0);
270
271 const SampleType* start = m_channelPointers[channel] + startFrame;
272 const SampleType* peakSample = std::max_element (
273 start,
274 start + numFrames,
275 [] (SampleType a, SampleType b) { return std::abs (a) < std::abs (b); }
276 );
277
278 return std::abs (*peakSample);
279 }
280
281 /// @brief Get the maximum absolute value in the buffer across all channels
282 /// @param startFrame The beginnig of the range of the frames to look for magnitude
283 /// @param numFrames Number of frames, beginning with startFrame, to look for the magnitude
284 /// @return The maximum value, in linear domain (i.e. not decibels)
285 SampleType getMagnitude (size_t startFrame, size_t numFrames) const
286 {
287 if (startFrame + numFrames > m_numFrames || numFrames == 0)
288 HART_THROW_OR_RETURN (hart::IndexError, "Invalid frame range", (SampleType) 0);
289
290 SampleType peakSampleAcrossAllChannels = (SampleType) 0;
291
292 for (size_t channel = 0; channel < m_numChannels; ++channel)
293 {
294 const SampleType* start = m_channelPointers[channel] + startFrame;
295 const SampleType* peakSample = std::max_element (
296 start,
297 start + numFrames,
298 [] (SampleType a, SampleType b) { return std::abs (a) < std::abs (b); }
299 );
300 peakSampleAcrossAllChannels = std::max (peakSampleAcrossAllChannels, std::abs (*peakSample));
301 }
302
303 return peakSampleAcrossAllChannels;
304 }
305
306 // TODO: Implement resize() to avoid repeated memory re-allocations caused by spamming appendFrom()
307
308 /// @brief Copies audio from another buffer
309 /// @param destChannel Channel within this buffer to copy the frames to
310 /// @param destStartFrame Start frame within this buffer's channel
311 /// @param source Source buffer to read from
312 /// @param sourceChannel Channel within the source buffer to read from
313 /// @param sourceStartFrame Offset within the source buffer's channel to start reading frames from
314 /// @param numFrames Number of frames to copy
315 void copyFrom (size_t destChannel, size_t destStartFrame, const AudioBuffer& source, size_t sourceChannel, size_t sourceStartFrame, size_t numFrames)
316 {
317 if (destChannel >= m_numChannels || sourceChannel >= source.m_numChannels)
318 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Invalid channel");
319
320 if (destStartFrame + numFrames > m_numFrames || sourceStartFrame + numFrames > source.m_numFrames)
321 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Invalid frame range");
322
323 // Not a huge deal, but copying from a buffer with different SR must be at least a little suspicious
324 hassert ((! hasSampleRate() || ! source.hasSampleRate()) || floatsEqual (getSampleRateHz(), source.getSampleRateHz()));
325
326 std::copy (
327 source.m_channelPointers[sourceChannel] + sourceStartFrame,
328 source.m_channelPointers[sourceChannel] + sourceStartFrame + numFrames,
329 m_channelPointers[destChannel] + destStartFrame
330 );
331 }
332
333 /// @brief Copies audio from another generic audio buffer
334 /// @param destChannel Channel within this buffer to copy the frames to
335 /// @param destStartFrame Start frame within this buffer's channel
336 /// @param source Pointer to the source sample data, must contain at least `numFrames` samples
337 /// @param numFrames Number of frames to copy
338 void copyFrom (size_t destChannel, size_t destStartFrame, const SampleType* source, size_t numFrames)
339 {
340 if (destChannel >= m_numChannels)
341 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Invalid destination channel");
342
343 if (destStartFrame + numFrames > m_numFrames)
344 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Invalid frame range");
345
346 std::copy (source, source + numFrames, m_channelPointers[destChannel] + destStartFrame);
347 }
348
349 /// @brief Clears the entire buffer
350 /// @details Sets all frames in all channels to zeros, keeping the sample rate value intact
351 void clear()
352 {
353 std::fill (m_frames.begin(), m_frames.end(), (SampleType) 0);
354 }
355
356 /// @brief Clears a specific section of a given channel
357 /// @details Overwrites a selected section of the channel with zeros
358 /// @param channel Cnannel in which to clear a frame range
359 /// @param startFrame Start of the frame range to clear (inclusive)
360 /// @param numFrames Amount of frames to clear
361 void clear (size_t channel, size_t startFrame, size_t numFrames)
362 {
363 if (channel >= m_numChannels)
364 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Invalid channel");
365
366 if (startFrame + numFrames > m_numFrames)
367 HART_THROW_OR_RETURN_VOID (hart::IndexError, "Invalid frame range");
368
369 std::fill (m_channelPointers[channel], m_channelPointers[channel] + numFrames, (SampleType) 0);
370 }
371
372 /// @brief Prints readable representation of the audio buffer
373 /// @param stream String stream to append the representation to
374 void represent (std::ostream& stream) const
375 {
376 std::ostringstream oss;
377 stream << "AudioBuffer (" << m_numChannels << ", " << m_numFrames;
378
379 if (hasSampleRate())
380 stream << ", " << hzPrecision << m_sampleRateHz << ')';
381 else
382 stream << ", nan())";
383 }
384
385private:
386 size_t m_numChannels = 0;
387 size_t m_numFrames = 0;
388 double m_sampleRateHz = nan<double>();
389 std::vector<SampleType> m_frames;
390 std::vector<SampleType*> m_channelPointers;
391
392 void updateChannelPointers()
393 {
394 for (size_t channel = 0; channel < m_numChannels; ++channel)
395 m_channelPointers[channel] = m_numFrames > 0 ? &m_frames[channel * m_numFrames] : nullptr;
396 }
397};
398
399} // namespace hart
Container for audio data.
void clear(size_t channel, size_t startFrame, size_t numFrames)
Clears a specific section of a given channel.
SampleType * operator[](size_t channel)
Get a raw pointer to a specific channel's mutable audio data.
static AudioBuffer emptyLike(const AudioBuffer &other)
Creates an empty audio buffer with the same number of channels, frames and sample rate as the other b...
AudioBuffer & operator=(AudioBuffer &&other)
Creates an audio buffer by move-assigning.
size_t getNumFrames() const
Get number of frames (samples)
AudioBuffer & operator=(const AudioBuffer &other)
Creates an audio buffer by copy-assigning.
AudioBuffer(size_t numChannels=0, size_t numFrames=0, double sampleRateHz=nan< double >())
Creates an audio buffer.
void appendFrom(const AudioBuffer< SampleType > &otherBuffer)
Appends data from another buffer.
double getLengthSeconds() const
Get a duration of the audio buffer.
bool hasSampleRate() const
Check if a specific sample rate was assigned to the audio buffer.
void setNumFrames(size_t newNumFrames)
Resizes the buffer to hold a new number of frames per channel.
void copyFrom(size_t destChannel, size_t destStartFrame, const SampleType *source, size_t numFrames)
Copies audio from another generic audio buffer.
void copyFrom(size_t destChannel, size_t destStartFrame, const AudioBuffer &source, size_t sourceChannel, size_t sourceStartFrame, size_t numFrames)
Copies audio from another buffer.
double getSampleRateHz() const
Get a sample rate metadata.
const SampleType *const * getArrayOfReadPointers() const
Gets a raw pointer to the read-only audio data.
SampleType getMagnitude(size_t startFrame, size_t numFrames) const
Get the maximum absolute value in the buffer across all channels.
SampleType *const * getArrayOfWritePointers()
Gets a raw pointer to the mutable audio data.
AudioBuffer(const AudioBuffer &other)
Creates an audio buffer by copying.
~AudioBuffer()=default
The destructor.
const SampleType * operator[](size_t channel) const
Get a raw pointer to a specific channel's read-only audio data.
AudioBuffer(AudioBuffer &&other)
Creates an audio buffer by moving.
void represent(std::ostream &stream) const
Prints readable representation of the audio buffer.
SampleType getMagnitude(size_t channel, size_t startFrame, size_t numFrames) const
Get the maximum absolute value in the buffer in a specific channel.
void clear()
Clears the entire buffer.
size_t getNumChannels() const
Get number of channels.
void erase()
Clears the buffer.
Thrown when a numbers of channels is mismatched.
Thrown when a container index is out of range.
#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_THROW_OR_RETURN(ExceptionType, message, returnValue)
Throws an exception if HART_DO_NOT_THROW_EXCEPTIONS is set, prints a message and returns a specified ...
std::ostream & hzPrecision(std::ostream &stream)
Sets number of decimal places for values in hertz.
FloatType nan()
Returns a quiet NaN value for the given floating-point type.
static SampleType floatsEqual(SampleType a, SampleType b, SampleType epsilon=(SampleType) 1e-8)
Compares two floating point numbers within a given tolerance.