DSPark 1.6.1
Header-only audio DSP framework in pure C++20 — zero dependencies
Loading...
Searching...
No Matches
RingBuffer.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
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
37namespace dspark {
38
42enum class InterpMethod
43{
44 Linear,
45 Cubic,
46 Hermite,
48};
49
56template <FloatType T>
58{
59public:
65 RingBuffer() noexcept = default;
66
78 void prepare(int maxSamples) noexcept
79 {
80 assert(maxSamples > 0);
81
82 // Clamp before the round-up loop: growing the signed capacity past
83 // 2^30 would overflow the shift (undefined) and never terminate.
84 constexpr int kMaxCapacity = 1 << 30;
85 if (maxSamples > kMaxCapacity) maxSamples = kMaxCapacity;
86
87 capacity_ = 1;
88 while (capacity_ < maxSamples)
89 capacity_ <<= 1;
90
91 mask_ = capacity_ - 1;
92
93 // Ensure 32-byte alignment for SIMD operations. std::aligned_alloc
94 // additionally requires the size to be an integral multiple of the
95 // alignment (UB / nullptr otherwise on POSIX), so round bytes up to 32.
96 size_t bytes = static_cast<size_t>(capacity_) * sizeof(T);
97 bytes = (bytes + 31u) & ~static_cast<size_t>(31u);
98
99 // Custom deleter for aligned allocation
100 buffer_.reset(static_cast<T*>(
101 #if defined(_MSC_VER)
102 _aligned_malloc(bytes, 32)
103 #else
104 std::aligned_alloc(32, bytes)
105 #endif
106 ));
107
108 // On allocation failure leave the buffer in a safe unprepared state.
109 if (!buffer_) { capacity_ = 0; mask_ = 0; writePos_ = 0; return; }
110
111 reset();
112 }
113
118 void reset() noexcept
119 {
120 if (buffer_)
121 std::fill_n(buffer_.get(), capacity_, T(0));
122 writePos_ = 0;
123 }
124
129 inline void push(T sample) noexcept
130 {
131 if (capacity_ == 0) [[unlikely]] return; // not prepared
132 buffer_[writePos_] = sample;
133 writePos_ = (writePos_ + 1) & mask_;
134 }
135
146 void pushBlock(const T* samples, int numSamples) noexcept
147 {
148 if (capacity_ == 0 || numSamples <= 0) [[unlikely]] return;
149
150 if (numSamples >= capacity_)
151 {
152 // Only the trailing capacity_ samples survive a full overwrite.
153 samples += numSamples - capacity_;
154 std::memcpy(buffer_.get(), samples, static_cast<size_t>(capacity_) * sizeof(T));
155 writePos_ = 0;
156 return;
157 }
158
159 const int firstSpan = std::min(numSamples, capacity_ - writePos_);
160 std::memcpy(buffer_.get() + writePos_, samples,
161 static_cast<size_t>(firstSpan) * sizeof(T));
162
163 const int remaining = numSamples - firstSpan;
164 if (remaining > 0)
165 std::memcpy(buffer_.get(), samples + firstSpan,
166 static_cast<size_t>(remaining) * sizeof(T));
167
168 writePos_ = (writePos_ + numSamples) & mask_;
169 }
170
179 [[nodiscard]] inline T read(int delaySamples) const noexcept
180 {
181 if (capacity_ == 0) [[unlikely]] return T(0); // not prepared
182 assert(delaySamples >= 0 && delaySamples < capacity_
183 && "RingBuffer::read: delay out of range");
184
185 int idx = (writePos_ - 1 - delaySamples) & mask_;
186 return buffer_[idx];
187 }
188
209 template <InterpMethod Method = InterpMethod::Cubic>
210 [[nodiscard]] inline T readInterpolated(T delaySamples) const noexcept
211 {
212 if (capacity_ == 0) [[unlikely]] return T(0); // not prepared
213
214 int intDelay = static_cast<int>(delaySamples);
215 T frac = delaySamples - static_cast<T>(intDelay);
216
217 assert(delaySamples >= T(0) && intDelay + 2 < capacity_);
218
219 if constexpr (Method == InterpMethod::Linear)
220 {
221 T s0 = read(intDelay);
222 T s1 = read(intDelay + 1);
223 return s0 + frac * (s1 - s0);
224 }
225 else
226 {
227 assert(delaySamples >= T(1) && "4-point interpolators require delay >= 1.0");
228
229 T s[4];
230 // Optimized contiguous logical read
231 s[0] = read(intDelay - 1);
232 s[1] = read(intDelay);
233 s[2] = read(intDelay + 1);
234 s[3] = read(intDelay + 2);
235
236 // Interpolate within the s[1]..s[2] interval, so an integer delay
237 // (frac == 0) returns exactly read(intDelay) == s[1]. The 4-point
238 // (y0,y1,y2,y3,frac) overloads do precisely that. (Calling the
239 // buffer+position overloads with `frac` would misread it as an
240 // absolute index and interpolate s[0]..s[1] with a wrapped neighbour,
241 // i.e. a 1-sample delay error.)
242 if constexpr (Method == InterpMethod::Cubic || Method == InterpMethod::Hermite)
243 return interpolateHermite(s[0], s[1], s[2], s[3], frac);
244 else if constexpr (Method == InterpMethod::Lagrange)
245 return interpolateLagrange(s[0], s[1], s[2], s[3], frac);
246 }
247 }
248
249 [[nodiscard]] int getCapacity() const noexcept { return capacity_; }
250 [[nodiscard]] const T* data() const noexcept { return buffer_.get(); }
251 [[nodiscard]] int getWritePosition() const noexcept { return writePos_; }
252
253private:
254 struct AlignedDeleter {
255 void operator()(T* ptr) const {
256 #if defined(_MSC_VER)
257 _aligned_free(ptr);
258 #else
259 std::free(ptr);
260 #endif
261 }
262 };
263
264 std::unique_ptr<T[], AlignedDeleter> buffer_;
265 int capacity_ = 0;
266 int mask_ = 0;
267 int writePos_ = 0;
268};
269
270} // namespace dspark
Core mathematical utilities for digital signal processing.
Fractional-position interpolation for delay lines, tables and buffers.
Power-of-two circular buffer with compile-time interpolated read access.
Definition RingBuffer.h:58
T read(int delaySamples) const noexcept
Reads a sample at an integer delay.
Definition RingBuffer.h:179
void prepare(int maxSamples) noexcept
Allocates the ring buffer with the given capacity.
Definition RingBuffer.h:78
const T * data() const noexcept
Definition RingBuffer.h:250
void pushBlock(const T *samples, int numSamples) noexcept
Pushes a block of samples into the buffer.
Definition RingBuffer.h:146
void push(T sample) noexcept
Pushes a single sample into the buffer.
Definition RingBuffer.h:129
int getCapacity() const noexcept
Definition RingBuffer.h:249
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:118
T readInterpolated(T delaySamples) const noexcept
Reads a sample at a fractional delay using the specified interpolation.
Definition RingBuffer.h:210
int getWritePosition() const noexcept
Definition RingBuffer.h:251
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:43
@ 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)