DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
RingBuffer.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
27#include "DspMath.h"
28#include "Interpolation.h"
29
30#include <algorithm>
31#include <cassert>
32#include <cstddef>
33#include <cstdlib>
34#include <cstring>
35#include <memory>
36
37#if defined(_WIN32)
38 #include <malloc.h> // _aligned_malloc / _aligned_free (MSVC and MinGW CRTs)
39#endif
40
41namespace dspark {
42
46enum class InterpMethod
47{
48 Linear,
49 Cubic,
50 Hermite,
52};
53
60template <FloatType T>
62{
63public:
69 RingBuffer() noexcept = default;
70
82 void prepare(int maxSamples) noexcept
83 {
84 assert(maxSamples > 0);
85
86 // Clamp before the round-up loop: growing the signed capacity past
87 // 2^30 would overflow the shift (undefined) and never terminate.
88 constexpr int kMaxCapacity = 1 << 30;
89 if (maxSamples > kMaxCapacity) maxSamples = kMaxCapacity;
90
91 capacity_ = 1;
92 while (capacity_ < maxSamples)
93 capacity_ <<= 1;
94
95 mask_ = capacity_ - 1;
96
97 // Ensure 32-byte alignment for SIMD operations. std::aligned_alloc
98 // additionally requires the size to be an integral multiple of the
99 // alignment (UB / nullptr otherwise on POSIX), so round bytes up to 32.
100 size_t bytes = static_cast<size_t>(capacity_) * sizeof(T);
101 bytes = (bytes + 31u) & ~static_cast<size_t>(31u);
102
103 // Custom deleter for aligned allocation
104 buffer_.reset(static_cast<T*>(
105 // The Windows CRT has no std::aligned_alloc, whatever the compiler
106 // (MSVC, clang-cl, MinGW GCC/Clang): it has _aligned_malloc.
107 #if defined(_WIN32)
108 _aligned_malloc(bytes, 32)
109 #else
110 std::aligned_alloc(32, bytes)
111 #endif
112 ));
113
114 // On allocation failure leave the buffer in a safe unprepared state.
115 if (!buffer_) { capacity_ = 0; mask_ = 0; writePos_ = 0; return; }
116
117 reset();
118 }
119
124 void reset() noexcept
125 {
126 if (buffer_)
127 std::fill_n(buffer_.get(), capacity_, T(0));
128 writePos_ = 0;
129 }
130
135 inline void push(T sample) noexcept
136 {
137 if (capacity_ == 0) [[unlikely]] return; // not prepared
138 buffer_[writePos_] = sample;
139 writePos_ = (writePos_ + 1) & mask_;
140 }
141
152 void pushBlock(const T* samples, int numSamples) noexcept
153 {
154 if (capacity_ == 0 || numSamples <= 0) [[unlikely]] return;
155
156 if (numSamples >= capacity_)
157 {
158 // Only the trailing capacity_ samples survive a full overwrite.
159 samples += numSamples - capacity_;
160 std::memcpy(buffer_.get(), samples, static_cast<size_t>(capacity_) * sizeof(T));
161 writePos_ = 0;
162 return;
163 }
164
165 const int firstSpan = std::min(numSamples, capacity_ - writePos_);
166 std::memcpy(buffer_.get() + writePos_, samples,
167 static_cast<size_t>(firstSpan) * sizeof(T));
168
169 const int remaining = numSamples - firstSpan;
170 if (remaining > 0)
171 std::memcpy(buffer_.get(), samples + firstSpan,
172 static_cast<size_t>(remaining) * sizeof(T));
173
174 writePos_ = (writePos_ + numSamples) & mask_;
175 }
176
185 [[nodiscard]] inline T read(int delaySamples) const noexcept
186 {
187 if (capacity_ == 0) [[unlikely]] return T(0); // not prepared
188 assert(delaySamples >= 0 && delaySamples < capacity_
189 && "RingBuffer::read: delay out of range");
190
191 int idx = (writePos_ - 1 - delaySamples) & mask_;
192 return buffer_[idx];
193 }
194
215 template <InterpMethod Method = InterpMethod::Cubic>
216 [[nodiscard]] inline T readInterpolated(T delaySamples) const noexcept
217 {
218 if (capacity_ == 0) [[unlikely]] return T(0); // not prepared
219
220 int intDelay = static_cast<int>(delaySamples);
221 T frac = delaySamples - static_cast<T>(intDelay);
222
223 assert(delaySamples >= T(0) && intDelay + 2 < capacity_);
224
225 if constexpr (Method == InterpMethod::Linear)
226 {
227 T s0 = read(intDelay);
228 T s1 = read(intDelay + 1);
229 return s0 + frac * (s1 - s0);
230 }
231 else
232 {
233 assert(delaySamples >= T(1) && "4-point interpolators require delay >= 1.0");
234
235 T s[4];
236 // Optimized contiguous logical read
237 s[0] = read(intDelay - 1);
238 s[1] = read(intDelay);
239 s[2] = read(intDelay + 1);
240 s[3] = read(intDelay + 2);
241
242 // Interpolate within the s[1]..s[2] interval, so an integer delay
243 // (frac == 0) returns exactly read(intDelay) == s[1]. The 4-point
244 // (y0,y1,y2,y3,frac) overloads do precisely that. (Calling the
245 // buffer+position overloads with `frac` would misread it as an
246 // absolute index and interpolate s[0]..s[1] with a wrapped neighbour,
247 // i.e. a 1-sample delay error.)
248 if constexpr (Method == InterpMethod::Cubic || Method == InterpMethod::Hermite)
249 return interpolateHermite(s[0], s[1], s[2], s[3], frac);
250 else if constexpr (Method == InterpMethod::Lagrange)
251 return interpolateLagrange(s[0], s[1], s[2], s[3], frac);
252 }
253 }
254
255 [[nodiscard]] int getCapacity() const noexcept { return capacity_; }
256 [[nodiscard]] const T* data() const noexcept { return buffer_.get(); }
257 [[nodiscard]] int getWritePosition() const noexcept { return writePos_; }
258
259private:
260 struct AlignedDeleter {
261 void operator()(T* ptr) const {
262 #if defined(_WIN32)
263 _aligned_free(ptr);
264 #else
265 std::free(ptr);
266 #endif
267 }
268 };
269
270 std::unique_ptr<T[], AlignedDeleter> buffer_;
271 int capacity_ = 0;
272 int mask_ = 0;
273 int writePos_ = 0;
274};
275
276} // namespace dspark
Power-of-two circular buffer with compile-time interpolated read access.
Definition RingBuffer.h:62
T read(int delaySamples) const noexcept
Reads a sample at an integer delay.
Definition RingBuffer.h:185
void prepare(int maxSamples) noexcept
Allocates the ring buffer with the given capacity.
Definition RingBuffer.h:82
const T * data() const noexcept
Definition RingBuffer.h:256
void pushBlock(const T *samples, int numSamples) noexcept
Pushes a block of samples into the buffer.
Definition RingBuffer.h:152
void push(T sample) noexcept
Pushes a single sample into the buffer.
Definition RingBuffer.h:135
int getCapacity() const noexcept
Definition RingBuffer.h:255
RingBuffer() noexcept=default
Constructs an empty ring buffer. No allocation happens until prepare() is called, honouring the frame...
void reset() noexcept
Clears the buffer to zero without deallocating. Safe to call from the audio thread.
Definition RingBuffer.h:124
T readInterpolated(T delaySamples) const noexcept
Reads a sample at a fractional delay using the specified interpolation.
Definition RingBuffer.h:216
int getWritePosition() const noexcept
Definition RingBuffer.h:257
Main namespace for the DSPark framework.
T interpolateHermite(T y0, T y1, T y2, T y3, T frac) noexcept
4-point, 3rd-order Hermite interpolation (optimized x-form).
T interpolateLagrange(T y0, T y1, T y2, T y3, T frac) noexcept
4-point Lagrange interpolation from discrete samples.
InterpMethod
Interpolation method for fractional-sample reads.
Definition RingBuffer.h:47
@ Lagrange
4-point Lagrange (highest accuracy, static delay)
@ Linear
2-point linear (fast, lower quality)
@ Hermite
4-point Hermite (smooth transients, best for modulated delay)
@ Cubic
4-point Catmull-Rom (good balance)