DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
AudioBuffer.h
1// DSPark -- Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi -- MIT License
3
4#pragma once
5
23#include "SimdOps.h"
24
25#include <algorithm>
26#include <array>
27#include <bit>
28#include <cassert>
29#include <cstddef>
30#include <cstring>
31#include <new>
32#include <type_traits>
33#include <utility>
34
35namespace dspark {
36
37// ============================================================================
38// AudioBufferView -- Non-owning view
39// ============================================================================
40
48template <typename T, int MaxViewChannels = 16>
50{
51public:
53 AudioBufferView() noexcept = default;
54
62 template <typename U>
63 requires std::is_convertible_v<U*, T*>
64 AudioBufferView(U* const* channelPtrs, int numChannels, int numSamples) noexcept
65 {
66 assert(numChannels >= 0 && numChannels <= MaxViewChannels);
67
68 // Release-safe clamp: an out-of-range channel count must never write
69 // past the fixed pointer array (assert-only protection vanishes in
70 // release builds).
71 numChannels_ = std::clamp(numChannels, 0, MaxViewChannels);
72 numSamples_ = numSamples >= 0 ? numSamples : 0;
73
74 for (int ch = 0; ch < numChannels_; ++ch)
75 channels_[ch] = channelPtrs[ch];
76 }
77
99 template <typename U, int OtherChannels>
100 requires (std::is_convertible_v<U*, T*> && OtherChannels <= MaxViewChannels)
102 : numChannels_(other.getNumChannels()), numSamples_(other.getNumSamples())
103 {
104 for (int ch = 0; ch < numChannels_; ++ch)
105 channels_[ch] = other.getChannel(ch);
106 }
107
108 // -- Accessors -----------------------------------------------------------
109
111 [[nodiscard]] T* getChannel(int ch) const noexcept
112 {
113 assert(ch >= 0 && ch < numChannels_);
114 return channels_[ch];
115 }
116
118 [[nodiscard]] int getNumChannels() const noexcept { return numChannels_; }
119
121 [[nodiscard]] int getNumSamples() const noexcept { return numSamples_; }
122
123 // -- Sub-views -----------------------------------------------------------
124
131 [[nodiscard]] AudioBufferView getSubView(int startSample, int length) const noexcept
132 {
133 assert(startSample >= 0);
134 assert(length >= 0);
135 assert(startSample + length <= numSamples_);
136
137 AudioBufferView sub;
138 sub.numChannels_ = numChannels_;
139 sub.numSamples_ = length;
140 for (int ch = 0; ch < numChannels_; ++ch)
141 sub.channels_[ch] = channels_[ch] + startSample;
142
143 return sub;
144 }
145
146 // -- Operations ----------------------------------------------------------
147
149 void clear() const noexcept
150 {
151 // A hard error, not a silent no-op: matching applyGain/copyFrom so a
152 // clear() on a read-only view can never be mistaken for a real one.
153 static_assert(!std::is_const_v<T>, "Cannot clear a const view");
154 const auto bytes = static_cast<std::size_t>(numSamples_) * sizeof(T);
155 // An empty view may hold null channel pointers (a zero-length host
156 // block, an empty AudioBuffer); memset on null is undefined even with
157 // a zero length.
158 if (bytes == 0) return;
159 for (int ch = 0; ch < numChannels_; ++ch)
160 std::memset(channels_[ch], 0, bytes);
161 }
162
174 template <typename U, int M>
175 void copyFrom(const AudioBufferView<U, M>& src) const noexcept
176 {
177 static_assert(!std::is_const_v<T>, "Cannot copy into a const view");
178
179 const int chCount = std::min(numChannels_, src.getNumChannels());
180 const int nSamples = std::min(numSamples_, src.getNumSamples());
181
182 for (int ch = 0; ch < chCount; ++ch)
183 {
184 T* dst = channels_[ch];
185 const U* s = src.getChannel(ch);
186
187 if constexpr (std::is_same_v<std::remove_const_t<U>, T>)
188 {
189 std::memmove(dst, s, static_cast<std::size_t>(nSamples) * sizeof(T));
190 }
191 else
192 {
193 for (int i = 0; i < nSamples; ++i)
194 dst[i] = static_cast<T>(s[i]);
195 }
196 }
197 }
198
211 template <typename U, int M>
212 void addFrom(const AudioBufferView<U, M>& src, T gain = T(1)) const noexcept
213 {
214 static_assert(!std::is_const_v<T>, "Cannot add into a const view");
215
216 const int chCount = std::min(numChannels_, src.getNumChannels());
217 const int nSamples = std::min(numSamples_, src.getNumSamples());
218
219 for (int ch = 0; ch < chCount; ++ch)
220 {
221 T* dst = channels_[ch];
222 const U* s = src.getChannel(ch);
223
224 if constexpr (std::is_same_v<std::remove_const_t<U>, T>)
225 {
226 simd::addWithGain(dst, s, gain, nSamples);
227 }
228 else
229 {
230 for (int i = 0; i < nSamples; ++i)
231 dst[i] += static_cast<T>(s[i]) * gain;
232 }
233 }
234 }
235
240 void applyGain(T gain) const noexcept
241 {
242 static_assert(!std::is_const_v<T>, "Cannot apply gain to a const view");
243 for (int ch = 0; ch < numChannels_; ++ch)
244 simd::applyGain(channels_[ch], gain, numSamples_);
245 }
246
251 [[nodiscard]] std::remove_const_t<T> getPeakLevel() const noexcept
252 {
253 // remove_const keeps the accumulator (and the returned value)
254 // assignable when T is a const sample type: this read-only query
255 // must work on read-only views.
256 using Value = std::remove_const_t<T>;
257 Value peak = Value(0);
258 for (int ch = 0; ch < numChannels_; ++ch)
259 {
260 const Value chPeak = simd::peakLevel(channels_[ch], numSamples_);
261 if (chPeak > peak) peak = chPeak;
262 }
263 return peak;
264 }
265
266private:
267 std::array<T*, MaxViewChannels> channels_ {};
268 int numChannels_ = 0;
269 int numSamples_ = 0;
270};
271
272// The view is part of the hot-path calling convention: it must stay cheap to
273// copy by value. Guard the promise made in the file header.
274static_assert(std::is_trivially_copyable_v<AudioBufferView<float>>,
275 "AudioBufferView must remain trivially copyable");
276static_assert(std::is_trivially_copyable_v<AudioBufferView<const float>>,
277 "AudioBufferView must remain trivially copyable");
278
279// ============================================================================
280// AudioBuffer -- Owning, SIMD-aligned buffer
281// ============================================================================
282
290template <typename T, int MaxChannels = 16>
292{
293 static constexpr std::size_t kAlignment = 32;
294 static_assert(std::has_single_bit(kAlignment), "Alignment must be a power of two");
295 static_assert(std::is_same_v<T, std::remove_cv_t<T>> && std::is_trivially_copyable_v<T>,
296 "AudioBuffer requires a non-const, trivially copyable sample type");
297
298public:
299 AudioBuffer() = default;
300
301 ~AudioBuffer() { deallocate(); }
302
303 AudioBuffer(const AudioBuffer&) = delete;
305
307 AudioBuffer(AudioBuffer&& other) noexcept
308 : rawData_(std::exchange(other.rawData_, nullptr))
309 , channelPtrs_(std::exchange(other.channelPtrs_, {}))
310 , numChannels_(std::exchange(other.numChannels_, 0))
311 , numSamples_(std::exchange(other.numSamples_, 0))
312 , allocatedBytes_(std::exchange(other.allocatedBytes_, 0))
313 , strideBytes_(std::exchange(other.strideBytes_, 0))
314 {}
315
318 {
319 if (this != &other)
320 {
321 deallocate();
322 rawData_ = std::exchange(other.rawData_, nullptr);
323 channelPtrs_ = std::exchange(other.channelPtrs_, {});
324 numChannels_ = std::exchange(other.numChannels_, 0);
325 numSamples_ = std::exchange(other.numSamples_, 0);
326 allocatedBytes_ = std::exchange(other.allocatedBytes_, 0);
327 strideBytes_ = std::exchange(other.strideBytes_, 0);
328 }
329 return *this;
330 }
331
332 // -- Allocation ----------------------------------------------------------
333
345 void resize(int numChannels, int numSamples)
346 {
347 assert(numChannels >= 0 && numChannels <= MaxChannels);
348 assert(numSamples >= 0);
349
350 // Release-safe clamps: out-of-range requests must never overflow the
351 // fixed channel-pointer array or produce a negative allocation size.
352 numChannels = std::clamp(numChannels, 0, MaxChannels);
353 if (numSamples < 0) numSamples = 0;
354
355 const auto samplesBytes = static_cast<std::size_t>(numSamples) * sizeof(T);
356 const auto stride = alignUp(samplesBytes, kAlignment);
357 const auto totalBytes = stride * static_cast<std::size_t>(numChannels);
358
359 // Smart re-allocation: reuse memory if requested size fits in capacity
360 if (totalBytes > allocatedBytes_)
361 {
362 deallocate();
363 rawData_ = static_cast<T*>(::operator new(totalBytes, std::align_val_t(kAlignment)));
364 allocatedBytes_ = totalBytes;
365 }
366
367 numChannels_ = numChannels;
368 numSamples_ = numSamples;
369 strideBytes_ = stride;
370
371 auto* base = reinterpret_cast<char*>(rawData_);
372
373 // Map active channels
374 for (int ch = 0; ch < numChannels; ++ch)
375 channelPtrs_[ch] = reinterpret_cast<T*>(base + stride * static_cast<std::size_t>(ch));
376
377 // Nullify unused channels for safety
378 for (int ch = numChannels; ch < MaxChannels; ++ch)
379 channelPtrs_[ch] = nullptr;
380
381 clear(); // Guarantees padding zeroing for SIMD safety
382 }
383
384 // -- View creation -------------------------------------------------------
385
391 {
392 return { channelPtrs_.data(), numChannels_, numSamples_ };
393 }
394
396 [[nodiscard]] AudioBufferView<const T, MaxChannels> toView() const noexcept
397 {
398 return { channelPtrs_.data(), numChannels_, numSamples_ };
399 }
400
401 // -- Accessors -----------------------------------------------------------
402
404 [[nodiscard]] T* getChannel(int ch) noexcept
405 {
406 assert(ch >= 0 && ch < numChannels_);
407 return channelPtrs_[ch];
408 }
409
411 [[nodiscard]] const T* getChannel(int ch) const noexcept
412 {
413 assert(ch >= 0 && ch < numChannels_);
414 return channelPtrs_[ch];
415 }
416
418 [[nodiscard]] int getNumChannels() const noexcept { return numChannels_; }
419
421 [[nodiscard]] int getNumSamples() const noexcept { return numSamples_; }
422
423 // -- Operations ----------------------------------------------------------
424
429 void clear() noexcept
430 {
431 // Degenerate sizes (0 samples or 0 channels) own no storage at all;
432 // memset on a null pointer is undefined even with a zero length.
433 if (rawData_ == nullptr) return;
434 for (int ch = 0; ch < numChannels_; ++ch)
435 std::memset(channelPtrs_[ch], 0, strideBytes_);
436 }
437
438private:
439 [[nodiscard]] static constexpr std::size_t alignUp(std::size_t value, std::size_t alignment) noexcept
440 {
441 return (value + alignment - 1) & ~(alignment - 1);
442 }
443
444 void deallocate() noexcept
445 {
446 if (rawData_)
447 {
448 ::operator delete(rawData_, std::align_val_t(kAlignment));
449 rawData_ = nullptr;
450 }
451 // Keep the capacity coherent with the (now absent) allocation. If
452 // resize()'s operator new throws after this call, a stale capacity
453 // would make a later, smaller resize() skip its allocation and build
454 // channel pointers over a null base.
455 allocatedBytes_ = 0;
456 // Drop dangling channel views too: if operator new throws in resize()
457 // right after this call, channelPtrs_/numChannels_ would otherwise
458 // still describe the just-freed block, so a caller catching bad_alloc
459 // and calling getChannel()/toView() would read freed memory. Leave the
460 // object as a safely-empty buffer. The success path re-establishes
461 // these fields after a successful allocation.
462 channelPtrs_.fill(nullptr);
463 numChannels_ = 0;
464 numSamples_ = 0;
465 strideBytes_ = 0;
466 }
467
468 T* rawData_ = nullptr;
469 std::array<T*, MaxChannels> channelPtrs_ {};
470 int numChannels_ = 0;
471 int numSamples_ = 0;
472 std::size_t allocatedBytes_ = 0;
473 std::size_t strideBytes_ = 0;
474};
475
476} // namespace dspark
Non-owning view over audio channel data.
Definition AudioBuffer.h:50
void clear() const noexcept
Fills the valid sample range of all channels with zeros.
int getNumSamples() const noexcept
Returns the number of samples per channel.
int getNumChannels() const noexcept
Returns the number of channels in this view.
AudioBufferView() noexcept=default
Creates an empty view (0 channels, 0 samples).
void addFrom(const AudioBufferView< U, M > &src, T gain=T(1)) const noexcept
Adds samples from a source view into this view with optional gain.
T * getChannel(int ch) const noexcept
Returns a pointer to the sample data for the given channel.
AudioBufferView(const AudioBufferView< U, OtherChannels > &other) noexcept
Converts compatible sample types and widens pointer-array capacity.
void copyFrom(const AudioBufferView< U, M > &src) const noexcept
Safely copies samples from a source view into this view.
AudioBufferView getSubView(int startSample, int length) const noexcept
Returns a zero-copy sub-view starting at an offset.
std::remove_const_t< T > getPeakLevel() const noexcept
Returns the peak absolute sample value across all channels.
void applyGain(T gain) const noexcept
Multiplies all samples in all valid channels by a gain factor.
Owning audio buffer with contiguous, 32-byte aligned storage.
AudioBuffer & operator=(const AudioBuffer &)=delete
AudioBufferView< T, MaxChannels > toView() noexcept
Returns a non-owning mutable view of this buffer. The view's channel capacity is propagated from MaxC...
int getNumChannels() const noexcept
Returns the number of active channels.
AudioBuffer(const AudioBuffer &)=delete
T * getChannel(int ch) noexcept
Returns a pointer to the sample data.
int getNumSamples() const noexcept
Returns the number of samples per channel.
AudioBufferView< const T, MaxChannels > toView() const noexcept
Returns a non-owning const view of this buffer.
AudioBuffer(AudioBuffer &&other) noexcept
C++20 idiomatic move constructor.
void resize(int numChannels, int numSamples)
Allocates the buffer for the given dimensions.
const T * getChannel(int ch) const noexcept
Const overload.
void clear() noexcept
Clears all channels, including SIMD alignment padding. Ensures any SIMD over-read will consume pure z...
AudioBuffer & operator=(AudioBuffer &&other) noexcept
C++20 idiomatic move assignment.
void addWithGain(float *DSPARK_RESTRICT dst, const float *DSPARK_RESTRICT src, float gain, int count) noexcept
Adds source samples scaled by a gain factor into a destination buffer.
Definition SimdOps.h:77
float peakLevel(const float *DSPARK_RESTRICT data, int count) noexcept
Returns the peak absolute sample value in a buffer.
Definition SimdOps.h:264
void applyGain(float *DSPARK_RESTRICT data, float gain, int count) noexcept
Multiplies all samples in a buffer by a gain factor.
Definition SimdOps.h:186
Main namespace for the DSPark framework.