DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
AudioBuffer.h
Go to the documentation of this file.
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 AudioBufferView(U* const* channelPtrs, int numChannels, int numSamples) noexcept
64 {
65 static_assert(std::is_convertible_v<U*, T*>, "Pointer type U* must be convertible to T*");
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
87 template <typename U>
89 : numChannels_(other.getNumChannels()), numSamples_(other.getNumSamples())
90 {
91 static_assert(std::is_convertible_v<U*, T*>, "Cannot convert view (e.g., const to non-const)");
92
93 for (int ch = 0; ch < numChannels_; ++ch)
94 channels_[ch] = other.getChannel(ch);
95 }
96
97 // -- Accessors -----------------------------------------------------------
98
100 [[nodiscard]] T* getChannel(int ch) const noexcept
101 {
102 assert(ch >= 0 && ch < numChannels_);
103 return channels_[ch];
104 }
105
107 [[nodiscard]] int getNumChannels() const noexcept { return numChannels_; }
108
110 [[nodiscard]] int getNumSamples() const noexcept { return numSamples_; }
111
112 // -- Sub-views -----------------------------------------------------------
113
120 [[nodiscard]] AudioBufferView getSubView(int startSample, int length) const noexcept
121 {
122 assert(startSample >= 0);
123 assert(length >= 0);
124 assert(startSample + length <= numSamples_);
125
126 AudioBufferView sub;
127 sub.numChannels_ = numChannels_;
128 sub.numSamples_ = length;
129 for (int ch = 0; ch < numChannels_; ++ch)
130 sub.channels_[ch] = channels_[ch] + startSample;
131
132 return sub;
133 }
134
135 // -- Operations ----------------------------------------------------------
136
138 void clear() const noexcept
139 {
140 // A hard error, not a silent no-op: matching applyGain/copyFrom so a
141 // clear() on a read-only view can never be mistaken for a real one.
142 static_assert(!std::is_const_v<T>, "Cannot clear a const view");
143 const auto bytes = static_cast<std::size_t>(numSamples_) * sizeof(T);
144 for (int ch = 0; ch < numChannels_; ++ch)
145 std::memset(channels_[ch], 0, bytes);
146 }
147
159 template <typename U, int M>
160 void copyFrom(const AudioBufferView<U, M>& src) const noexcept
161 {
162 static_assert(!std::is_const_v<T>, "Cannot copy into a const view");
163
164 const int chCount = std::min(numChannels_, src.getNumChannels());
165 const int nSamples = std::min(numSamples_, src.getNumSamples());
166
167 for (int ch = 0; ch < chCount; ++ch)
168 {
169 T* dst = channels_[ch];
170 const U* s = src.getChannel(ch);
171
172 if constexpr (std::is_same_v<std::remove_const_t<U>, T>)
173 {
174 std::memmove(dst, s, static_cast<std::size_t>(nSamples) * sizeof(T));
175 }
176 else
177 {
178 for (int i = 0; i < nSamples; ++i)
179 dst[i] = static_cast<T>(s[i]);
180 }
181 }
182 }
183
196 template <typename U, int M>
197 void addFrom(const AudioBufferView<U, M>& src, T gain = T(1)) const noexcept
198 {
199 static_assert(!std::is_const_v<T>, "Cannot add into a const view");
200
201 const int chCount = std::min(numChannels_, src.getNumChannels());
202 const int nSamples = std::min(numSamples_, src.getNumSamples());
203
204 for (int ch = 0; ch < chCount; ++ch)
205 {
206 T* dst = channels_[ch];
207 const U* s = src.getChannel(ch);
208
209 if constexpr (std::is_same_v<std::remove_const_t<U>, T>)
210 {
211 simd::addWithGain(dst, s, gain, nSamples);
212 }
213 else
214 {
215 for (int i = 0; i < nSamples; ++i)
216 dst[i] += static_cast<T>(s[i]) * gain;
217 }
218 }
219 }
220
225 void applyGain(T gain) const noexcept
226 {
227 static_assert(!std::is_const_v<T>, "Cannot apply gain to a const view");
228 for (int ch = 0; ch < numChannels_; ++ch)
229 simd::applyGain(channels_[ch], gain, numSamples_);
230 }
231
236 [[nodiscard]] std::remove_const_t<T> getPeakLevel() const noexcept
237 {
238 // remove_const keeps the accumulator (and the returned value)
239 // assignable when T is a const sample type: this read-only query
240 // must work on read-only views.
241 using Value = std::remove_const_t<T>;
242 Value peak = Value(0);
243 for (int ch = 0; ch < numChannels_; ++ch)
244 {
245 const Value chPeak = simd::peakLevel(channels_[ch], numSamples_);
246 if (chPeak > peak) peak = chPeak;
247 }
248 return peak;
249 }
250
251private:
252 std::array<T*, MaxViewChannels> channels_ {};
253 int numChannels_ = 0;
254 int numSamples_ = 0;
255};
256
257// The view is part of the hot-path calling convention: it must stay cheap to
258// copy by value. Guard the promise made in the file header.
259static_assert(std::is_trivially_copyable_v<AudioBufferView<float>>,
260 "AudioBufferView must remain trivially copyable");
261static_assert(std::is_trivially_copyable_v<AudioBufferView<const float>>,
262 "AudioBufferView must remain trivially copyable");
263
264// ============================================================================
265// AudioBuffer -- Owning, SIMD-aligned buffer
266// ============================================================================
267
275template <typename T, int MaxChannels = 16>
277{
278 static constexpr std::size_t kAlignment = 32;
279 static_assert(std::has_single_bit(kAlignment), "Alignment must be a power of two");
280 static_assert(std::is_same_v<T, std::remove_cv_t<T>> && std::is_trivially_copyable_v<T>,
281 "AudioBuffer requires a non-const, trivially copyable sample type");
282
283public:
284 AudioBuffer() = default;
285
286 ~AudioBuffer() { deallocate(); }
287
288 AudioBuffer(const AudioBuffer&) = delete;
290
292 AudioBuffer(AudioBuffer&& other) noexcept
293 : rawData_(std::exchange(other.rawData_, nullptr))
294 , channelPtrs_(std::exchange(other.channelPtrs_, {}))
295 , numChannels_(std::exchange(other.numChannels_, 0))
296 , numSamples_(std::exchange(other.numSamples_, 0))
297 , allocatedBytes_(std::exchange(other.allocatedBytes_, 0))
298 , strideBytes_(std::exchange(other.strideBytes_, 0))
299 {}
300
303 {
304 if (this != &other)
305 {
306 deallocate();
307 rawData_ = std::exchange(other.rawData_, nullptr);
308 channelPtrs_ = std::exchange(other.channelPtrs_, {});
309 numChannels_ = std::exchange(other.numChannels_, 0);
310 numSamples_ = std::exchange(other.numSamples_, 0);
311 allocatedBytes_ = std::exchange(other.allocatedBytes_, 0);
312 strideBytes_ = std::exchange(other.strideBytes_, 0);
313 }
314 return *this;
315 }
316
317 // -- Allocation ----------------------------------------------------------
318
330 void resize(int numChannels, int numSamples)
331 {
332 assert(numChannels >= 0 && numChannels <= MaxChannels);
333 assert(numSamples >= 0);
334
335 // Release-safe clamps: out-of-range requests must never overflow the
336 // fixed channel-pointer array or produce a negative allocation size.
337 numChannels = std::clamp(numChannels, 0, MaxChannels);
338 if (numSamples < 0) numSamples = 0;
339
340 const auto samplesBytes = static_cast<std::size_t>(numSamples) * sizeof(T);
341 const auto stride = alignUp(samplesBytes, kAlignment);
342 const auto totalBytes = stride * static_cast<std::size_t>(numChannels);
343
344 // Smart re-allocation: reuse memory if requested size fits in capacity
345 if (totalBytes > allocatedBytes_)
346 {
347 deallocate();
348 rawData_ = static_cast<T*>(::operator new(totalBytes, std::align_val_t(kAlignment)));
349 allocatedBytes_ = totalBytes;
350 }
351
352 numChannels_ = numChannels;
353 numSamples_ = numSamples;
354 strideBytes_ = stride;
355
356 auto* base = reinterpret_cast<char*>(rawData_);
357
358 // Map active channels
359 for (int ch = 0; ch < numChannels; ++ch)
360 channelPtrs_[ch] = reinterpret_cast<T*>(base + stride * static_cast<std::size_t>(ch));
361
362 // Nullify unused channels for safety
363 for (int ch = numChannels; ch < MaxChannels; ++ch)
364 channelPtrs_[ch] = nullptr;
365
366 clear(); // Guarantees padding zeroing for SIMD safety
367 }
368
369 // -- View creation -------------------------------------------------------
370
376 {
377 return { channelPtrs_.data(), numChannels_, numSamples_ };
378 }
379
381 [[nodiscard]] AudioBufferView<const T, MaxChannels> toView() const noexcept
382 {
383 return { channelPtrs_.data(), numChannels_, numSamples_ };
384 }
385
386 // -- Accessors -----------------------------------------------------------
387
389 [[nodiscard]] T* getChannel(int ch) noexcept
390 {
391 assert(ch >= 0 && ch < numChannels_);
392 return channelPtrs_[ch];
393 }
394
396 [[nodiscard]] const T* getChannel(int ch) const noexcept
397 {
398 assert(ch >= 0 && ch < numChannels_);
399 return channelPtrs_[ch];
400 }
401
403 [[nodiscard]] int getNumChannels() const noexcept { return numChannels_; }
404
406 [[nodiscard]] int getNumSamples() const noexcept { return numSamples_; }
407
408 // -- Operations ----------------------------------------------------------
409
414 void clear() noexcept
415 {
416 // Degenerate sizes (0 samples or 0 channels) own no storage at all;
417 // memset on a null pointer is undefined even with a zero length.
418 if (rawData_ == nullptr) return;
419 for (int ch = 0; ch < numChannels_; ++ch)
420 std::memset(channelPtrs_[ch], 0, strideBytes_);
421 }
422
423private:
424 [[nodiscard]] static constexpr std::size_t alignUp(std::size_t value, std::size_t alignment) noexcept
425 {
426 return (value + alignment - 1) & ~(alignment - 1);
427 }
428
429 void deallocate() noexcept
430 {
431 if (rawData_)
432 {
433 ::operator delete(rawData_, std::align_val_t(kAlignment));
434 rawData_ = nullptr;
435 }
436 // Keep the capacity coherent with the (now absent) allocation. If
437 // resize()'s operator new throws after this call, a stale capacity
438 // would make a later, smaller resize() skip its allocation and build
439 // channel pointers over a null base.
440 allocatedBytes_ = 0;
441 // Drop dangling channel views too: if operator new throws in resize()
442 // right after this call, channelPtrs_/numChannels_ would otherwise
443 // still describe the just-freed block, so a caller catching bad_alloc
444 // and calling getChannel()/toView() would read freed memory. Leave the
445 // object as a safely-empty buffer. The success path re-establishes
446 // these fields after a successful allocation.
447 channelPtrs_.fill(nullptr);
448 numChannels_ = 0;
449 numSamples_ = 0;
450 strideBytes_ = 0;
451 }
452
453 T* rawData_ = nullptr;
454 std::array<T*, MaxChannels> channelPtrs_ {};
455 int numChannels_ = 0;
456 int numSamples_ = 0;
457 std::size_t allocatedBytes_ = 0;
458 std::size_t strideBytes_ = 0;
459};
460
461} // namespace dspark
SIMD-accelerated buffer operations for real-time audio processing.
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, MaxViewChannels > &other) noexcept
Converting constructor allowing mutable to const view conversions.
Definition AudioBuffer.h:88
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.