|
- #pragma once
- #include <atomic>
-
- #include <dsp/common.hpp>
-
-
- 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 <typename T, size_t S>
- struct RingBuffer {
- std::atomic<size_t> start{0};
- std::atomic<size_t> 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 <typename T, size_t S>
- struct DoubleRingBuffer {
- std::atomic<size_t> start{0};
- std::atomic<size_t> 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 <typename T, size_t S, size_t N>
- 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
|