Let us analyze your application - now have a free initial consultation.

Are you looking for a company to support or support your Delphi project?
Click here!

Jetzt Anfrage einreichenTelefon ERPwerk Button.fw

Generic ring memory

Nils Eilers, 23.06.2017

Delphi Ring2 

Grijjy, Inc. Consulting on GitHub: Ring Buffers

Grijjy, Inc. Consulting provides various libraries, components, and source code for classes used in their application development on GitHub. Part of this offering includes generic ring buffers, which are described in the following post:


What Is a Ring Buffer?

A ring buffer is a fixed-size buffer whose beginning and end are connected, forming a continuous loop. It resembles a queue with its FIFO (First-In-First-Out) behavior. However, unlike a queue, it does not grow when the buffer is full. This makes ring buffers very fast and memory-efficient.

Ring buffers are ideal for buffering data that is received unevenly but must be provided consistently. You can see this in action every time you watch or listen to streaming media: The media player buffers a few milliseconds to seconds of incoming data to be able to play it back at a steady rate. In media applications, such a buffer is also known as a jitter buffer, because it compensates for jitter.

We use a ring buffer in our Grijjy application to buffer audio for playback and voice recording.


An Example of a Ring Buffer Could Look Like This:

(You may now proceed to describe or implement the example if needed.)

Ringbuffer1

This ring buffer has a fixed size of 16 elements (letters in this case). We have inserted 11 characters (A – K), so 5 are left.

We read from the ring buffer at the read pointer. For example, if we read 2 elements, we receive the characters A and B and move the read pointer forward by 2.

ringbuffer2

The ring buffer now has space for 7 more elements, which we can store at the write pointer. After 5 additional elements, we begin to overwrite the positions previously occupied by the characters A and B. For example, if we store 6 more characters, the buffer will look like this:

ringbuffer3

Here's the English translation:


A Generic Ring Buffer

Ring buffers can be useful for storing data of various types. Most commonly, you’ll find ring buffers that store generic data as byte buffers. However, in audio applications, for instance, it makes more sense to buffer audio samples, which are typically stored as 16-bit integers (Int16 or SmallInt in Delphi) or as single-precision floating-point values (Single in Delphi).

When audio is stored in an interleaved stereo format, each sample is represented as a record with two values (one for the left channel and one for the right channel).

So why not create a generic ring buffer that can be used with different types? We've created one for you called TgoRingBuffer<T>. You can find it in the Grijjy.Collections unit within the GrijjyFoundation repository.


Using the Ring Buffer

The API for the ring buffer is fairly simple:

type
  TgoRingBuffer<T> = class
  public
    constructor Create(const ACapacity: Integer);

    function Write(const AData: TArray<T>): Integer; overload;
    function Write(const AData: array of T): Integer; overload;
    function Write(const AData: array of T; const AIndex, ACount: Integer): Integer; overload;
    function TryWrite(const AData: array of T): Boolean; overload;
    function TryWrite(const AData: array of T; const AIndex, ACount: Integer): Boolean; overload;

    function Read(var AData: array of T): Integer; overload;
    function Read(var AData: array of T; const AIndex, ACount: Integer): Integer; overload;
    function TryRead(var AData: array of T): Boolean; overload;
    function TryRead(var AData: array of T; const AIndex, ACount: Integer): Boolean; overload;

    property Capacity: Integer read FCapacity;
    property Count: Integer read FCount;
    property Available: Integer read GetAvailable;
  end;

Explanation of the Ring Buffer API

The ring buffer is created by passing the capacity to the constructor.

  • This is the number of elements (of type T) that the ring buffer can hold.

  • The buffer will never exceed this capacity.

Properties:

  • Capacity: Returns the maximum number of elements that the ring buffer can hold.

  • Count: Returns the number of elements currently in the buffer (available for reading).

  • Available: Returns the number of available (free) items in the ring buffer (available for writing).

Note: All properties return the number of elements (of type T), not the number of bytes.


Read and Write Methods

The rest of the API consists of read and write methods with various overloads:

  1. Array Flexibility:
    Each method comes in two versions:

    • One that accepts data as a dynamic array (TArray<T>).

    • One that accepts data as an open array (array of T).
      This allows for greater flexibility.

  2. Pre-Allocated Array Reading:
    The Read methods do not create an array. Instead, they expect a pre-allocated array (either static or dynamic).

  3. Regular vs. Try Methods:

    • The regular methods attempt to read or write a specified number of items.

    • If insufficient items (or space) are available, they will read or write as many as possible and return the actual count.

    • The Try* methods either succeed or fail completely. They attempt to read or write the specified number of items and return a boolean result indicating success or failure.

  4. Reading and Writing Segments:
    Each method has an overload for working with only a segment of an array.

    • AIndex: The starting index in the array.

    • ACount: The number of elements to read (or write), starting from AIndex.


Difference from Other Collections

Unlike other (generic) collections such as Lists, Stacks, or Queues, this ring buffer does not work with individual values. Instead, it reads or writes arrays of values.

This highlights that the ring buffer is typically used for streaming or buffering applications.


Example Usage

Let's say we want to replicate the previous example. We'll create a ring buffer with 16 elements of type Char and write 11 letters (A to K) into it:

var
  RingBuffer: TgoRingBuffer<Char>;
begin
  RingBuffer := TgoRingBuffer<Char>.Create(16);
  RingBuffer.Write(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']);
  ...
end;

Explanation of Ring Buffer Implementation

  • The ring buffer maintains a fixed capacity and operates in a circular manner.

  • When the buffer is full, new data overwrites the oldest data.

  • Data can be written to or read from the buffer using array operations.

The TgoRingBuffer<T> implementation is particularly useful for:

  • Audio processing.

  • Network communication buffers.

  • Streaming data handling.

The source code is available on Grijjy’s GitHub repository.


Original post: “Expand your Collections collection – Part 2: a generic Ring Buffer” by Erik van Bilsen on Grijjy's website.
Translated by Nils Eilers.

 Ringbuffer1

We can also represent this as a "flat" array (which is actually the way the ring memory is implemented):

 ringbuffer flat1

Explanation of the Code Snippet

The code snippet demonstrates how to read data from a TgoRingBuffer<Char> into a static array.


Code

{ Reserve some space to read data from the ring buffer: }
var
    ReadBuffer: array [0..9] of Char;  { A static array with a capacity of 10 characters }
    Count: Integer;

{ Read data }
Count := RingBuffer.Read(ReadBuffer, 0, 2);  { Attempts to read 2 characters starting from index 0 }
Assert(Count = 2);  { Ensures exactly 2 characters were read }

Explanation

  1. Declare a Static Array:

    ReadBuffer: array [0..9] of Char;
    

    This defines a static array that can hold up to 10 characters.

  2. Reading Data from the Ring Buffer:

    Count := RingBuffer.Read(ReadBuffer, 0, 2);
    
    • The method attempts to read 2 characters from the ring buffer.

    • It starts writing data into the ReadBuffer at position 0.

    • The Read method returns the actual number of characters read, stored in the variable Count.

  3. Checking the Result:

    Assert(Count = 2);
    
    • This assertion ensures that exactly 2 characters were read from the ring buffer.

    • If the ring buffer contains fewer than 2 characters, the Read method will return a lower value, causing the assertion to fail.


Summary

The Read method provides a flexible way to read data from the ring buffer. By passing a static array, you ensure that the data is read directly into memory without requiring dynamic memory allocation. This approach is particularly useful in performance-critical applications such as audio processing or network communication.

Would you like me to show you how to read more data or use the other API variations? 😊

ringbuffer flat2

Explanation of the Code Snippet

You are now attempting to write 6 new elements into the ring buffer. Let's break down what's happening.


Code

Count := RingBuffer.Write(['L', 'M', 'N', 'O', 'P', 'Q']);
Assert(Count = 6);

What’s Happening Here?

  1. Attempting to Write New Data:

    Count := RingBuffer.Write(['L', 'M', 'N', 'O', 'P', 'Q']);
    
    • This line writes 6 new characters into the ring buffer.

    • The Write method returns the number of elements successfully written, which is stored in the variable Count.

  2. Assertion Check:

    Assert(Count = 6);
    
    • This ensures that all 6 characters were successfully written to the buffer.

    • If the ring buffer is full or does not have enough available space, this assertion will fail.


Understanding the Ring Buffer State

  • Ring Buffer Capacity: 16 elements.

  • Initial Elements: 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'.

  • You previously read the first 2 elements ('A', 'B'), freeing 2 positions.

  • Now, there are 5 more free positions available, making a total of:

    5 (empty) + 2 (previously `'A'` and `'B'`) = 7 free positions.
    
  • You are attempting to write 6 new elements.


Expected Outcome

  • Writing ['L', 'M', 'N', 'O', 'P', 'Q'] should succeed, as there are 7 free positions available.

  • The assertion Assert(Count = 6); should pass successfully.


📌 Summary

Your operation is valid because there is enough space in the buffer to accommodate all 6 elements. If you were to write more than 7 elements, the operation would partially succeed or fail, depending on whether you use a Write or TryWrite method.

Would you like me to show you how to read back these new elements from the buffer? 😊

ringbuffer flat3

Explanation of the Code Snippet

You are now attempting to write 3 new elements to the ring buffer, but only 1 will fit. Let's break down why this happens.


Code

Count := RingBuffer.Write(['R', 'S', 'T']);
Assert(Count = 1);

What’s Happening Here?

  1. Attempting to Write New Data:

    Count := RingBuffer.Write(['R', 'S', 'T']);
    
    • You are trying to write 3 characters ('R', 'S', 'T') to the buffer.

    • The Write method will write as many characters as it can until the buffer reaches its maximum capacity.

  2. Assertion Check:

    Assert(Count = 1);
    
    • This confirms that only 1 element was successfully written to the buffer.

    • If more than 1 element had been written, the assertion would fail.


Understanding the Ring Buffer State

  • Total Capacity: 16 elements.

  • Current Elements in Buffer: 11 initial items minus the 2 read (A, B), plus 6 new items:

    C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q
    
    • Total of 14 elements in the buffer.

  • Available Space:

    16 - 14 = 2 slots available.
    
  • You tried to write 3 elements.

  • Since only 2 slots were available, only 1 item ('R') fits.

  • The ring buffer now contains 15 elements.


Expected Outcome

  • The method Write(['R', 'S', 'T']) will only write 'R' to the buffer.

  • Count will be 1.

  • The assertion passes:

    Assert(Count = 1);
    

📌 Summary

  • The buffer is almost full, with only 1 slot available.

  • Writing 3 elements is impossible, so only 1 is written.

  • This is confirmed by the successful assertion.

Would you like me to help you read all the items back from the buffer to confirm the current state? 😊

ringbuffer flat4

An dieser Stelle ist der Ringspeicher voll und kann nicht weiter beschrieben werden, bis Daten gelesen werden. Es können nun bis zu 16 Elemente (C bis R) gelesen werden.

Anwendungshinweise:

Wie Sie vielleicht in der Record Constraint der Klassendeklaration gesehen haben, kann der Ringspeicher nur mit Wertetypen (Wie Integer, Character, Floating-Point, Enums und Records) genutzt werden:

type
    TgoRingBuffer<T: record> = class
    ...
    end;

Es ist nicht möglich, den Ringspeicher mit Referenztypen (Wie Strings und Objekte) zu verwenden.

Das ergibt Sinn, denn ein Ringspeicher wird zur Pufferung von Daten und nicht Objekten genutzt. Mit dieser Einschränkung können wir die Implementierung der Klasse optimieren und vereinfachen, da wir Referenztypen nicht berücksichtigen müssen.

Jetzt denken Sie vielleicht: „Na gut, dann verpacke ich einen Referenztyp einfach in einem Record“, wie hier:

type
   TWrappedString = record
        Value: String;
   end;

var
    RingBuffer: TgoRingBuffer;

Während das zwar kompiliert wird, wird eine Assertion in der Laufzeit ausgelöst, weil wir im Konstruktor prüfen, ob der Typ ein verwalteter Typ ist:

constructor TgoRingBuffer.Create(const ACapacity: Integer);
begin
    Assert(not IsManagedType(T));
    inherited Create;
    ...
end;

Die magische Kompilerfunktion IsManagedType gibt True zurück, wenn der gegebene Typ ein Speicherverwalteter Typ wie String oder Objekt ist. Sie gibt auch dann True zurück, wenn der Typ selbst nicht verwaltet ist (wie Records), aber einen verwalteten Typ enthält (Wie das String-Feld in dem obigen Beispiel).

Auf nicht-ARC-Plattformen werden Sie dennoch in der Lage sein, diesen „Hack“ zu benutzen, um ein Objekt zu verpacken. Das wird allerdings nicht auf ARC-Plattformen funktionieren, da diese auch Objekte verwalten.“

 

Nach dem Originalpost „Expand your Collections collection – Part 2: a generic ring buffer“, von Erik van Bilsen auf http://www.grijjy.com/, 12.01.2017. Übersetzung von Nils Eilers. Alle Bilder in der Übersetzung stammen ebenfalls aus dem Post „Expand your Collections collection – Part 2: a generic ring buffer“.