nimphea/nimphea_ringbuffer

Source   Edit  

Ring Buffer (Circular Buffer)

Pure Nim implementation of a lock-free ring buffer optimized for audio streaming.

Features:

  • Fixed capacity at compile time (no dynamic allocation)
  • Lock-free single-producer/single-consumer (SPSC)
  • Audio streaming optimized
  • Configurable overwrite behavior
  • Generic type support via templates
  • ⚠️ BREAKING CHANGE v0.9.1: Capacity N must be a power of 2 (2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, etc.)

Performance:

  • Uses bitwise AND instead of modulo (1 cycle vs 12-30 cycles on ARM Cortex-M7)
  • Compile-time validation ensures power-of-2 sizes

Use Cases:

  • Audio sample buffering (delay lines, loopers)
  • Inter-thread communication
  • Streaming data processing
  • Real-time data capture

Memory:

  • Stack allocated only
  • Size known at compile time
  • Zero runtime overhead

Migration from v0.9.0:

  • If you used non-power-of-2 sizes (e.g., 100, 500), round up to next power of 2
  • Example: RingBuffer[100, float32] → RingBuffer[128, float32]
  • Example: RingBuffer[500, float32] → RingBuffer[512, float32]

Usage:

import nimphea_ringbuffer

# Create ring buffer for 1024 samples (MUST be power of 2!)
var buffer: RingBuffer[1024, float32]
buffer.init()

# Write samples
discard buffer.write(0.5)
discard buffer.write(0.7)

# Read samples
var sample: float32
if buffer.read(sample):
  echo sample  # 0.5

# Block operations
var samples: array[64, float32]
let written = buffer.writeBlock(samples)
let readCount = buffer.readBlock(samples)

Types

OverwriteMode = enum
  OVERWRITE_OLDEST,         ## Overwrite oldest data (circular)
  REJECT_NEW                 ## Reject new writes when full
Behavior when buffer is full Source   Edit  
RingBuffer[N; T] = object

Lock-free circular buffer for audio streaming

⚠️ CRITICAL: N must be a power of 2 (2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, etc.) This is enforced at compile time for performance (bitwise AND vs modulo).

Generic Parameters:

  • N - Capacity (MUST be power of 2, checked at compile time)
  • T - Element type (typically float32 for audio)

Fields:

  • data - Fixed array storage
  • writeIdx - Write position
  • readIdx - Read position
  • mode - Overwrite behavior
Source   Edit  

Procs

proc `$`[N: static int; T](this: RingBuffer[N, T]): string

Convert ring buffer to string representation (for debugging)

Example:

echo buffer  # "RingBuffer[1024, float32](available=128/1024)"

Source   Edit  
proc available[N: static int; T](this: RingBuffer[N, T]): int {.inline.}

Get number of samples available to read

Returns: Number of readable samples

Example:

echo buffer.available()  # e.g., 128

Source   Edit  
proc capacity[N: static int; T](this: RingBuffer[N, T]): int {.inline.}

Get maximum capacity

Returns: Maximum capacity (compile-time constant N)

Source   Edit  
proc clear[N: static int; T](this: var RingBuffer[N, T])

Clear all data from the buffer

Example:

buffer.clear()

Source   Edit  
proc init[N: static int; T](this: var RingBuffer[N, T];
                            mode: OverwriteMode = OVERWRITE_OLDEST)

Initialize the ring buffer

Parameters:

  • mode - Overwrite behavior (default: OVERWRITE_OLDEST)
Source   Edit  
proc isEmpty[N: static int; T](this: RingBuffer[N, T]): bool {.inline.}

Check if buffer is empty

Returns: true if no data available

Source   Edit  
proc isFull[N: static int; T](this: RingBuffer[N, T]): bool {.inline.}

Check if buffer is full

Returns: true if no space remaining

Source   Edit  
proc peek[N: static int; T](this: RingBuffer[N, T]; value: var T;
                            offset: int = 0): bool {.inline.}

Peek at a value without removing it

Parameters:

  • value - Output parameter to receive the value
  • offset - Offset from current read position (default: 0)

Returns: true if successful, false if offset out of range

Example:

var sample: float32
if buffer.peek(sample, 5):
  echo "5 samples ahead: ", sample

Source   Edit  
proc read[N: static int; T](this: var RingBuffer[N, T]; value: var T): bool {.
    inline.}

Read a single value from the buffer

Parameters:

  • value - Output parameter to receive the value

Returns: true if successful, false if empty

Example:

var sample: float32
if buffer.read(sample):
  echo sample

Source   Edit  
proc readBlock[N: static int; T](this: var RingBuffer[N, T];
                                 data: var openArray[T]): int

Read multiple values from the buffer

Parameters:

  • data - Array to fill with read values

Returns: Number of values actually read

Example:

var samples: array[64, float32]
let count = buffer.readBlock(samples)
echo "Read ", count, " samples"

Source   Edit  
proc remaining[N: static int; T](this: RingBuffer[N, T]): int {.inline.}

Get number of free slots for writing

Returns: Number of writable slots

Example:

echo buffer.remaining()  # e.g., 384

Source   Edit  
proc write[N: static int; T](this: var RingBuffer[N, T]; value: T): bool {.
    inline.}

Write a single value to the buffer

Parameters:

  • value - Value to write

Returns: true if successful, false if full (REJECT_NEW mode)

Example:

if not buffer.write(0.5):
  echo "Buffer full"

Source   Edit  
proc writeBlock[N: static int; T](this: var RingBuffer[N, T]; data: openArray[T]): int

Write multiple values to the buffer

Parameters:

  • data - Array of values to write

Returns: Number of values actually written

Example:

var samples: array[64, float32]
# ... fill samples ...
let written = buffer.writeBlock(samples)
echo "Wrote ", written, " samples"

Source   Edit