#pragma once #include #include namespace rack { namespace dsp { /** Lock-free queue with fixed size and no allocations. If S is not a power of 2, performance might be reduced, and the index could overflow in a thousand years, but it should usually be fine for your purposes. Supports only a single producer and consumer. To my knowledge, nobody has invented a 100% correct multiple producer/consumer lock-free ring buffer for x86_64. */ template struct RingBuffer { std::atomic start{0}; std::atomic end{0}; T data[S]; /** Adds an element to the end of the buffer. */ void push(T t) { size_t i = end % S; data[i] = t; end++; } /** Copies an array to the end of the buffer. `n` must be at most S. */ void pushBuffer(const T* t, int n) { size_t i = end % S; size_t e1 = i + n; size_t e2 = (e1 < S) ? e1 : S; std::memcpy(&data[i], t, sizeof(T) * (e2 - i)); if (e1 > S) { std::memcpy(data, &t[S - i], sizeof(T) * (e1 - S)); } end += n; } /** Removes and returns an element from the start of the buffer. */ T shift() { size_t i = start % S; T t = data[i]; start++; return t; } /** Removes and copies an array from the start of the buffer. `n` must be at most S. */ void shiftBuffer(T* t, size_t n) { size_t i = start % S; size_t s1 = i + n; size_t s2 = (s1 < S) ? s1 : S; std::memcpy(t, &data[i], sizeof(T) * (s2 - i)); if (s1 > S) { std::memcpy(&t[S - i], data, sizeof(T) * (s1 - S)); } start += n; } void clear() { start = end.load(); } bool empty() const { return start >= end; } bool full() const { return end - start >= S; } size_t size() const { return end - start; } size_t capacity() const { return S - size(); } }; /** A cyclic buffer which maintains a valid linear array of size S by keeping a copy of the buffer in adjacent memory. This is not thread-safe. */ template struct DoubleRingBuffer { std::atomic start{0}; std::atomic end{0}; T data[2 * S]; void push(T t) { size_t i = end % S; data[i] = t; data[i + S] = t; end++; } T shift() { size_t i = start % S; T t = data[i]; start++; return t; } void clear() { start = end.load(); } bool empty() const { return start >= end; } bool full() const { return end - start >= S; } size_t size() const { return end - start; } size_t capacity() const { return S - size(); } /** Returns a pointer to S consecutive elements for appending. If any data is appended, you must call endIncr afterwards. Pointer is invalidated when any other method is called. */ T* endData() { size_t i = end % S; return &data[i]; } void endIncr(size_t n) { size_t i = end % S; size_t e1 = i + n; size_t e2 = (e1 < S) ? e1 : S; // Copy data forward std::memcpy(&data[S + i], &data[i], sizeof(T) * (e2 - i)); if (e1 > S) { // Copy data backward from the doubled block to the main block std::memcpy(data, &data[S], sizeof(T) * (e1 - S)); } end += n; } /** Returns a pointer to S consecutive elements for consumption If any data is consumed, call startIncr afterwards. */ const T* startData() const { size_t i = start % S; return &data[i]; } void startIncr(size_t n) { start += n; } }; /** A cyclic buffer which maintains a valid linear array of size S by sliding along a larger block of size N. This is not thread-safe. The linear array of S elements are moved back to the start of the block once it outgrows past the end. This happens every N - S pushes, so the push() time is O(1 + S / (N - S)). For example, a float buffer of size 64 in a block of size 1024 is nearly as efficient as RingBuffer. Not thread-safe. */ template struct AppleRingBuffer { size_t start = 0; size_t end = 0; T data[N]; void returnBuffer() { // move end block to beginning // may overlap, but memmove handles that correctly size_t s = size(); std::memmove(data, &data[start], sizeof(T) * s); start = 0; end = s; } void push(T t) { if (end + 1 > N) { returnBuffer(); } data[end++] = t; } T shift() { return data[start++]; } bool empty() const { return start == end; } bool full() const { return end - start == S; } size_t size() const { return end - start; } size_t capacity() const { return S - size(); } /** Returns a pointer to S consecutive elements for appending, requesting to append n elements. */ T* endData(size_t n) { if (end + n > N) { returnBuffer(); } return &data[end]; } /** Actually increments the end position Must be called after endData(), and `n` must be at most the `n` passed to endData() */ void endIncr(size_t n) { end += n; } /** Returns a pointer to S consecutive elements for consumption If any data is consumed, call startIncr afterwards. */ const T* startData() const { return &data[start]; } void startIncr(size_t n) { // This is valid as long as n < S start += n; } }; } // namespace dsp } // namespace rack