arduino-audio-tools
Loading...
Searching...
No Matches
RingBufferSPSC.h
Go to the documentation of this file.
1#pragma once
2#include <atomic>
3#include <cstring>
4
7
8namespace audio_tools {
9
51template <typename T = uint8_t>
52class RingBufferSPSC : public BaseBuffer<T> {
53 public:
54 RingBufferSPSC() = default;
55
56 explicit RingBufferSPSC(size_t capacity) { resize(capacity); }
57
58 // ── BaseBuffer interface ────────────────────────────────────────────────
59
60 bool write(T data) override { return writeArray(&data, 1) == 1; }
61
62 bool read(T& result) override { return readArray(&result, 1) == 1; }
63
64 bool peek(T& result) override {
65 size_t h = head_.load(std::memory_order_acquire);
66 size_t t = tail_.load(std::memory_order_relaxed);
67 if (h == t) return false;
68 result = buf_[t & mask_];
69 return true;
70 }
71
72 // Bulk write — called only by the producer.
73 int writeArray(const T data[], int len) override {
74 if (capacity_ == 0 || len <= 0) return 0;
75 size_t t = tail_.load(std::memory_order_acquire);
76 size_t h = head_.load(std::memory_order_relaxed);
77 size_t space = capacity_ - (h - t);
78 size_t n = (size_t)len < space ? (size_t)len : space;
79 if (n == 0) return 0;
80
81 T* buf = buf_.data();
82 size_t h_idx = h & mask_;
83 size_t first = capacity_ - h_idx; // bytes until end of physical buffer
84 if (first > n) first = n;
85 memcpy(buf + h_idx, data, first * sizeof(T));
86 memcpy(buf, data + first, (n - first) * sizeof(T));
87
88 head_.store(h + n, std::memory_order_release);
89 return (int)n;
90 }
91
92 // Bulk read — called only by the consumer.
93 int readArray(T data[], int len) override {
94 if (capacity_ == 0 || len <= 0) return 0;
95 size_t h = head_.load(std::memory_order_acquire);
96 size_t t = tail_.load(std::memory_order_relaxed);
97 size_t avail = h - t;
98 size_t n = (size_t)len < avail ? (size_t)len : avail;
99 if (n == 0) return 0;
100
101 T* buf = buf_.data();
102 size_t t_idx = t & mask_;
103 size_t first = capacity_ - t_idx; // bytes until end of physical buffer
104 if (first > n) first = n;
105 memcpy(data, buf + t_idx, first * sizeof(T));
106 memcpy(data + first, buf, (n - first) * sizeof(T));
107
108 tail_.store(t + n, std::memory_order_release);
109 return (int)n;
110 }
111
112 // available() is called from both sides (consumer AND feedback ISR on
113 // producer core), so we use acquire on both loads for a consistent
114 // snapshot regardless of which core is calling.
115 int available() override {
116 size_t h = head_.load(std::memory_order_acquire);
117 size_t t = tail_.load(std::memory_order_acquire);
118 return (int)(h - t);
119 }
120
121 int availableForWrite() override {
122 size_t h = head_.load(std::memory_order_acquire);
123 size_t t = tail_.load(std::memory_order_acquire);
124 return (int)(capacity_ - (h - t));
125 }
126
127 // reset() is only safe when neither producer nor consumer is active
128 // (e.g. USB bus reset). It is not atomic.
129 void reset() override {
130 head_.store(0, std::memory_order_relaxed);
131 tail_.store(0, std::memory_order_relaxed);
132 }
133
134 // Opt-in only - see the class comment. Call before the first resize();
135 // changing it after buf_ is already allocated only takes effect on the
136 // *next* resize() (the current allocation is left exactly as it is).
137 void setUsePSRAM(bool flag) { use_psram_ = flag; }
138
139 // NOT safe to call while the producer or consumer side may still be
140 // active - same requirement as reset() above, and for the same reason:
141 // it reallocates buf_ and touches capacity_/mask_ with no atomics/
142 // barriers of its own. Callers must ensure both sides are quiescent
143 // first (e.g. USB bus reset before either endpoint is re-armed).
144 bool resize(size_t capacity) override {
145 // RingBufferSPSC always discards its logical content on resize() (the
146 // reset() call below), so release any existing allocation - under
147 // whichever allocator it was actually made with - before switching
148 // allocators or reallocating. This guarantees a block is never freed
149 // through a different allocator instance than the one that allocated
150 // it (which setUsePSRAM() toggling between resize() calls would
151 // otherwise risk), and avoids Vector wastefully copying bytes forward
152 // across the resize that are about to be discarded anyway. It also
153 // means resize(0) actually releases the buffer instead of leaving the
154 // old allocation in place (Vector::resize(0) alone is a no-op).
155 buf_.reset();
156
157 if (capacity == 0) {
158 capacity_ = 0;
159 mask_ = 0;
160 reset();
161 return true;
162 }
163
164 // Round up to the next power of two so masking replaces modulo, using
165 // the same bounded bit-smear technique as QueueLockFree::resize()
166 // rather than a naive `while (pow2 < capacity) pow2 <<= 1;` - that
167 // loop never terminates once capacity exceeds the largest
168 // representable power of two (pow2 overflows to 0 and the condition
169 // stays true forever). The smear instead comes out as 0 in that case,
170 // which is detected and rejected below.
171 size_t pow2 = capacity - 1;
172 for (size_t i = 1; i <= sizeof(size_t) * 4; i <<= 1) pow2 |= pow2 >> i;
173 pow2 += 1;
174 if (pow2 == 0) {
175 capacity_ = 0;
176 mask_ = 0;
177 reset();
178 return false;
179 }
180
181 buf_.setAllocator(use_psram_ ? DefaultAllocator : DefaultAllocatorRAM);
182 bool allocated = buf_.resize(pow2) && buf_.data() != nullptr;
183 capacity_ = allocated ? pow2 : 0;
184 mask_ = capacity_ > 0 ? capacity_ - 1 : 0;
185 reset();
186 return allocated;
187 }
188
189 T* address() override { return buf_.data(); }
190 size_t size() override { return capacity_; }
191
192 ~RingBufferSPSC() override = default;
193
194 // Non-copyable: the atomics are not copyable.
197
198 private:
199 Vector<T> buf_;
200 size_t capacity_ = 0;
201 size_t mask_ = 0;
202 bool use_psram_ = false;
203
204 // Separate the two hot atomics onto different 32-byte regions to prevent
205 // false sharing on multi-core targets that have a data cache (e.g. ESP32).
206 // On Cortex-M0+ (RP2040, no cache) this is free.
207 alignas(32) std::atomic<size_t> head_{0}; // written by producer only
208 alignas(32) std::atomic<size_t> tail_{0}; // written by consumer only
209};
210
211} // namespace audio_tools
Shared functionality of all buffers.
Definition Buffers.h:23
Lock-free Single-Producer Single-Consumer ring buffer.
Definition RingBufferSPSC.h:52
RingBufferSPSC(const RingBufferSPSC &)=delete
size_t size() override
Definition RingBufferSPSC.h:190
bool peek(T &result) override
peeks the actual entry from the buffer
Definition RingBufferSPSC.h:64
bool read(T &result) override
reads a single value
Definition RingBufferSPSC.h:62
RingBufferSPSC(size_t capacity)
Definition RingBufferSPSC.h:56
bool write(T data) override
write add an entry to the buffer
Definition RingBufferSPSC.h:60
~RingBufferSPSC() override=default
int available() override
provides the number of entries that are available to read
Definition RingBufferSPSC.h:115
int availableForWrite() override
provides the number of entries that are available to write
Definition RingBufferSPSC.h:121
T * address() override
returns the address of the start of the physical read buffer
Definition RingBufferSPSC.h:189
int writeArray(const T data[], int len) override
Fills the buffer data.
Definition RingBufferSPSC.h:73
bool resize(size_t capacity) override
Resizes the buffer if supported: returns false if not supported.
Definition RingBufferSPSC.h:144
void setUsePSRAM(bool flag)
Definition RingBufferSPSC.h:137
RingBufferSPSC & operator=(const RingBufferSPSC &)=delete
void reset() override
clears the buffer
Definition RingBufferSPSC.h:129
int readArray(T data[], int len) override
reads multiple values
Definition RingBufferSPSC.h:93
Vector implementation which provides the most important methods as defined by std::vector....
Definition Vector.h:21
Generic Implementation of sound input and output for desktop environments using portaudio.
Definition LMSEchoCancellationStream.h:6
static TAllocatorExt DefaultAllocator
Definition Allocator.h:208
static TAllocatorSTD DefaultAllocatorRAM
Definition Allocator.h:209