ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Ring Buffer

🔁 Lock-free circular buffers for audio/video More...

Files

file  ringbuffer.c
 đŸŽŻ Lock-free circular buffer for audio streaming with atomic operations
 
file  ringbuffer.h
 Lock-Free Ring Buffer and Frame Buffer Management.
 

Data Structures

struct  ringbuffer_t
 Lock-free ring buffer structure. More...
 
struct  audio_ring_buffer
 Audio ring buffer for real-time audio streaming. More...
 
struct  frame_t
 Frame structure that stores both data and actual size. More...
 
struct  multi_source_frame_t
 Multi-source frame structure for multi-user support. More...
 
struct  framebuffer_t
 Frame buffer structure for managing video frames. More...
 

Typedefs

typedef struct audio_ring_buffer audio_ring_buffer_t
 Audio ring buffer for real-time audio streaming.
 

Functions

ringbuffer_t * ringbuffer_create (size_t element_size, size_t capacity)
 Create a new ring buffer.
 
void ringbuffer_destroy (ringbuffer_t *rb)
 Destroy a ring buffer and free its memory.
 
bool ringbuffer_write (ringbuffer_t *rb, const void *data)
 Try to write an element to the ring buffer (non-blocking)
 
bool ringbuffer_read (ringbuffer_t *rb, void *data)
 Try to read an element from the ring buffer (non-blocking)
 
bool ringbuffer_peek (ringbuffer_t *rb, void *data)
 Peek at the next element without removing it.
 
size_t ringbuffer_size (const ringbuffer_t *rb)
 Get current number of elements in the buffer.
 
bool ringbuffer_is_empty (const ringbuffer_t *rb)
 Check if buffer is empty.
 
bool ringbuffer_is_full (const ringbuffer_t *rb)
 Check if buffer is full.
 
void ringbuffer_clear (ringbuffer_t *rb)
 Clear all elements from the buffer.
 
framebuffer_t * framebuffer_create (size_t capacity)
 Create a frame buffer for ASCII frames.
 
framebuffer_t * framebuffer_create_multi (size_t capacity)
 Create a multi-source frame buffer for multi-user support.
 
void framebuffer_destroy (framebuffer_t *fb)
 Destroy a frame buffer.
 
bool framebuffer_write_frame (framebuffer_t *fb, const char *frame_data, size_t frame_size)
 Write a frame to the buffer.
 
bool framebuffer_read_frame (framebuffer_t *fb, frame_t *frame)
 Read a frame from the buffer.
 
void framebuffer_clear (framebuffer_t *fb)
 Clear all frames from the buffer, freeing their data.
 
bool framebuffer_write_multi_frame (framebuffer_t *fb, const char *frame_data, size_t frame_size, uint32_t source_client_id, uint32_t frame_sequence, uint32_t timestamp)
 Write a multi-source frame to the buffer (for multi-user support)
 
bool framebuffer_read_multi_frame (framebuffer_t *fb, multi_source_frame_t *frame)
 Read a multi-source frame from the buffer.
 
bool framebuffer_peek_latest_multi_frame (framebuffer_t *fb, multi_source_frame_t *frame)
 Peek at the latest multi-source frame without removing it.
 

Audio Ring Buffer Constants

#define AUDIO_RING_BUFFER_SIZE   192000
 Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)
 
#define AUDIO_JITTER_BUFFER_THRESHOLD   4800
 Jitter buffer threshold - samples needed before starting playback.
 
#define AUDIO_JITTER_LOW_WATER_MARK   2400
 Low water mark - warn when available drops below this.
 
#define AUDIO_JITTER_HIGH_WATER_MARK   (AUDIO_JITTER_BUFFER_THRESHOLD * 2)
 High water mark - drop OLD samples when buffer exceeds this.
 
#define AUDIO_JITTER_TARGET_LEVEL   (AUDIO_JITTER_BUFFER_THRESHOLD + 480)
 Target buffer level after dropping old samples.
 
#define AUDIO_CROSSFADE_SAMPLES   256
 Crossfade duration in samples for smooth underrun recovery.
 

Frame Magic Numbers

#define FRAME_MAGIC   MAGIC_FRAME_VALID
 Magic number indicating a valid frame (0xDEADBEEF) - Alias for backward compatibility.
 
#define FRAME_FREED   MAGIC_FRAME_FREED
 Magic number indicating a freed frame (0xFEEDFACE) - Alias for backward compatibility.
 

Detailed Description

🔁 Lock-free circular buffers for audio/video

This header provides lock-free ring buffer implementations for high-performance producer-consumer scenarios in ascii-chat. The system includes both generic lock-free ring buffers and specialized audio and frame buffers for media streaming.

CORE FEATURES:

RING BUFFER ARCHITECTURE:

The lock-free ring buffer uses atomic operations for:

AUDIO RING BUFFER:

Specialized audio ring buffer with:

FRAME BUFFER:

Frame buffer system for ASCII video frames:

PERFORMANCE BENEFITS:

THREAD SAFETY:

Note
Ring buffers are ideal for producer-consumer scenarios where one thread produces data and another consumes it.
The lock-free ring buffer requires power-of-2 capacity for optimal performance (uses bit masking instead of modulo).
Audio ring buffers use jitter buffering to compensate for network latency and packet timing variations.
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
August 2025

Ring Buffer

Overview

Welcome! Let's talk about the ring buffer—one of the coolest data structures in ascii-chat. If you've ever wondered how to pass data between threads really fast without all the overhead of mutexes and locks, you're in the right place.

Picture this: you have one thread capturing video frames (the producer) and another thread encoding them (the consumer). How do you hand frames from one to the other? You could use a mutex-protected queue, but that means locking and unlocking for every single frame. Not ideal when you're doing this 30 times per second!

That's where the ring buffer shines. It's a circular buffer that uses atomic operations instead of locks, making it blazingly fast for the single-producer, single-consumer pattern. Think of it like a race track where the producer and consumer chase each other around—one writing, one reading, never quite catching up to each other.

You can find the implementation in lib/ringbuffer.c and lib/ringbuffer.h.

What makes ring buffers special?

Ring buffers are designed from the ground up for speed. They use C11 atomics instead of mutexes, which means way less overhead when passing data between threads. The design is intentionally simple—no complex algorithms here, just a circular buffer done right.

The implementation takes advantage of a clever trick: power-of-2 capacities allow us to use bitwise masking instead of slow modulo operations. This might sound like a small detail, but it makes index calculations 15-30 times faster!

We also have specialized versions for different use cases. Frame buffers handle ASCII art frames with metadata like magic numbers for corruption detection. Audio ring buffers include jitter buffering to smooth out network hiccups. Each one is tailored to its specific job.

Architecture

Let's dive into how ring buffers actually work under the hood. The design is elegant in its simplicity—just a few atomic variables and a chunk of memory, but combined in a way that achieves lock-free thread safety.

Ring Buffer Structure

The core structure is pretty straightforward:

typedef struct {
char *buffer; // Actual memory
size_t element_size; // Size per element
size_t capacity; // Max elements (power of 2)
_Atomic size_t head; // Write position (producer)
_Atomic size_t tail; // Read position (consumer)
_Atomic size_t size; // Current element count
bool is_power_of_two; // Optimization flag
size_t capacity_mask; // Fast modulo mask
Lock-free ring buffer structure.
Definition ringbuffer.h:88

The key insight here is that head and tail are atomic variables. The producer writes at head and advances it, while the consumer reads from tail and advances it. Since each position is only accessed by one thread, we don't need locks—just atomic operations to make sure reads and writes are visible across threads.

The capacity_mask is the secret sauce for performance. When capacity is a power of 2, we can use head & capacity_mask instead of head % capacity. Bitwise AND is a single CPU instruction, while modulo takes 15-30 cycles. That adds up fast when you're doing it thousands of times per second!

Lock-Free Algorithm

The ring buffer uses atomic operations for thread-safe access without locks. Here's how the producer and consumer interact:

Write operation (producer):

When the producer wants to write data, it first checks if there's room in the buffer. If the buffer is full, we return false—simple as that. Otherwise, we calculate the write index using our fast bitwise mask trick, copy the data into place, and atomically advance the head.

bool ringbuffer_write(ringbuffer_t *rb, const void *data) {
size_t current_size = atomic_load(&rb->size);
if (current_size >= rb->capacity) {
return false; // Buffer full
}
size_t head = atomic_load(&rb->head);
size_t index = head & rb->capacity_mask; // Fast modulo
// Write data
memcpy(rb->buffer + (index * rb->element_size), data, rb->element_size);
// Advance head atomically
atomic_store(&rb->head, head + 1);
atomic_fetch_add(&rb->size, 1);
return true;
}
bool ringbuffer_write(ringbuffer_t *rb, const void *data)
Try to write an element to the ring buffer (non-blocking)
Definition ringbuffer.c:60
size_t capacity
Number of elements that can be stored.
Definition ringbuffer.h:94
atomic_t head
Write position (producer) - atomic for lock-free operations.
Definition ringbuffer.h:96
atomic_t size
Current number of elements - atomic for fast size checks.
Definition ringbuffer.h:100
size_t capacity_mask
Mask for fast modulo when capacity is power of 2.
Definition ringbuffer.h:104
size_t element_size
Size of each element in bytes.
Definition ringbuffer.h:92
char * buffer
The actual buffer memory.
Definition ringbuffer.h:90

Read operation (consumer):

The consumer side is the mirror image. We check if there's data available, calculate the read index the same way, copy data out, and atomically advance the tail.

bool ringbuffer_read(ringbuffer_t *rb, void *data) {
size_t current_size = atomic_load(&rb->size);
if (current_size == 0) {
return false; // Buffer empty
}
size_t tail = atomic_load(&rb->tail);
size_t index = tail & rb->capacity_mask; // Fast modulo
// Read data
memcpy(data, rb->buffer + (index * rb->element_size), rb->element_size);
// Advance tail atomically
atomic_store(&rb->tail, tail + 1);
atomic_fetch_sub(&rb->size, 1);
return true;
}
bool ringbuffer_read(ringbuffer_t *rb, void *data)
Try to read an element from the ring buffer (non-blocking)
Definition ringbuffer.c:82
atomic_t tail
Read position (consumer) - atomic for lock-free operations.
Definition ringbuffer.h:98

The beauty of this design is that the producer and consumer can run completely independently. They never block each other—the worst case is one returns false if the buffer is full or empty, but that's a check we'd need anyway.

Buffer Types

We have several flavors of ring buffer, each optimized for a specific use case. The generic ring buffer handles arbitrary element types, while frame and audio buffers are specialized for their domains.

Generic Ring Buffer

The generic ring buffer is your go-to choice when you need to pass arbitrary data types between threads. It's simple, fast, and gets the job done.

Here's a quick example of using it:

// Create ring buffer for 256 integers
ringbuffer_t *rb = ringbuffer_create(sizeof(int), 256);
// Producer thread
int value = 42;
if (!ringbuffer_write(rb, &value)) {
log_warn("Ring buffer full, dropping data");
}
// Consumer thread
int received;
if (ringbuffer_read(rb, &received)) {
printf("Received: %d\n", received);
}
// Cleanup
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
ringbuffer_t * ringbuffer_create(size_t element_size, size_t capacity)
Create a new ring buffer.
Definition ringbuffer.c:29
void ringbuffer_destroy(ringbuffer_t *rb)
Destroy a ring buffer and free its memory.
Definition ringbuffer.c:53

Notice how the capacity gets rounded up to the next power of 2 automatically (256 is already a power of 2, so it stays as-is). If you ask for 100 elements, you'll get 128 instead—a small price to pay for the performance benefits.

Frame Buffer

Frame buffers are specialized for ASCII art frames. They handle variable-sized frames and include metadata like magic numbers for corruption detection. This is crucial when frames are being passed between threads—we need to make sure we're not reading garbage if something goes wrong.

The frame structure looks like this:

typedef struct {
uint32_t magic; // FRAME_MAGIC (0xDEADBEEF) for corruption detection
size_t size; // Actual frame size
char *data; // Frame data (allocated from buffer pool, NOT owned by consumer)
typedef struct {
mutex_t mutex; // Protects frame data allocation/free
mutex_t mutex
unsigned int uint32_t
Definition common.h:58
Frame structure that stores both data and actual size.
Definition ringbuffer.h:383
Frame buffer structure for managing video frames.
Definition ringbuffer.h:436
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Note
Frame data is allocated from the buffer pool for efficiency. The buffer pool owns the memory - consumers should NOT free frame.data manually.

The magic number serves as a sanity check. When you read a frame, you can verify that frame.magic == FRAME_MAGIC to make sure you got a valid frame and not corrupted memory. It's a simple check, but it can catch a lot of subtle bugs!

Here's how you'd use it in practice:

// Create frame buffer (capacity = 60 frames)
// Producer: Write ASCII frame
char *ascii_frame = generate_ascii_art(...);
if (!framebuffer_write_frame(fb, ascii_frame, strlen(ascii_frame))) {
log_warn("Frame buffer full, dropping frame");
}
// Consumer: Read ASCII frame
if (frame.magic == FRAME_MAGIC) {
render_frame(frame.data, frame.size);
// DO NOT free frame.data - buffer pool owns it!
// Data is automatically freed when framebuffer_clear() or
// framebuffer_destroy() is called.
}
}
// Cleanup (automatically frees all frame data via buffer pool)
void framebuffer_destroy(framebuffer_t *fb)
Destroy a frame buffer.
Definition ringbuffer.c:202
framebuffer_t * framebuffer_create(size_t capacity)
Create a frame buffer for ASCII frames.
Definition ringbuffer.c:146
bool framebuffer_read_frame(framebuffer_t *fb, frame_t *frame)
Read a frame from the buffer.
Definition ringbuffer.c:275
#define FRAME_MAGIC
Magic number indicating a valid frame (0xDEADBEEF) - Alias for backward compatibility.
Definition ringbuffer.h:422
bool framebuffer_write_frame(framebuffer_t *fb, const char *frame_data, size_t frame_size)
Write a frame to the buffer.
Definition ringbuffer.c:221
int frame
Definition splash.c:99
Note
Frame data is managed by the buffer pool, NOT owned by the consumer. Do NOT call SAFE_FREE() on frame.data - it will be automatically freed when the buffer is cleared or destroyed.

Multi-Source Frame Buffer

When you have multiple clients sending frames, you need to track which client sent what. That's where the multi-source frame buffer comes in. It's essentially the same as a regular frame buffer, but each frame includes metadata about its source.

The multi-source frame structure tracks the client ID, frame sequence number, and timestamp:

typedef struct {
uint32_t magic;
uint32_t source_client_id; // Which client sent this
uint32_t frame_sequence; // Frame number
uint32_t timestamp; // Capture time
size_t size;
char *data;
Multi-source frame structure for multi-user support.
Definition ringbuffer.h:400

Using it is straightforward:

// Create multi-source frame buffer
// Producer: Write frame from client 3
framebuffer_write_multi_frame(fb, frame_data, frame_size,
3, // client_id
frame_seq,
timestamp);
// Consumer: Read frame
log_debug("Frame from client %u: seq=%u size=%zu",
frame.source_client_id, frame.frame_sequence, frame.size);
render_client_frame(frame.source_client_id, frame.data, frame.size);
}
#define SAFE_FREE(ptr)
Definition common.h:376
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
bool framebuffer_read_multi_frame(framebuffer_t *fb, multi_source_frame_t *frame)
Read a multi-source frame from the buffer.
Definition ringbuffer.c:425
framebuffer_t * framebuffer_create_multi(size_t capacity)
Create a multi-source frame buffer for multi-user support.
Definition ringbuffer.c:174
bool framebuffer_write_multi_frame(framebuffer_t *fb, const char *frame_data, size_t frame_size, uint32_t source_client_id, uint32_t frame_sequence, uint32_t timestamp)
Write a multi-source frame to the buffer (for multi-user support)
Definition ringbuffer.c:378

Audio Ring Buffer

Audio ring buffers are a bit different from the others. They're designed for real-time audio playback, which means they need to handle network jitter gracefully. The buffer accumulates samples before starting playback, creating a "jitter buffer" that smooths out irregularities in network timing.

The audio buffer uses a fixed size tuned for audio needs:

#define AUDIO_RING_BUFFER_SIZE (256 * 32) // 8192 samples = ~186ms @ 44.1kHz
#define AUDIO_JITTER_BUFFER_THRESHOLD (256 * 8) // Wait 46ms before playback
typedef struct audio_ring_buffer {
volatile int write_index;
volatile int read_index;
volatile bool jitter_buffer_filled; // True after initial fill
struct audio_ring_buffer audio_ring_buffer_t
#define AUDIO_RING_BUFFER_SIZE
Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)
Definition ringbuffer.h:128
Audio ring buffer for real-time audio streaming.
Definition ringbuffer.h:205
atomic_t jitter_buffer_filled
True after initial jitter buffer fill.
Definition ringbuffer.h:217
atomic_t read_index
Read index (consumer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:211
mutex_t mutex
Mutex for SLOW PATH only (clear/destroy operations, not regular read/write)
Definition ringbuffer.h:229
atomic_t write_index
Write index (producer position) - LOCK-FREE with atomic operations.
Definition ringbuffer.h:209
float data[192000]
Audio sample data buffer.
Definition ringbuffer.h:207

Why the jitter buffer?

Network audio packets don't arrive at perfectly regular intervals. Sometimes packets come early, sometimes they're delayed. Without buffering, you'd get audio crackling whenever a packet was late. The jitter buffer waits until it has enough samples to smooth out these timing variations before starting playback.

The threshold is set to about 46 milliseconds of audio. This is usually enough to handle typical network jitter without introducing noticeable latency. Once the buffer has reached this threshold, playback begins and the buffer continues to absorb timing variations.

Here's how it works in practice:

audio_ring_buffer_t *arb = audio_ringbuffer_create();
// Network receive thread: Write samples
void receive_audio(audio_ring_buffer_t *arb, float *samples, int count) {
mutex_lock(&arb->mutex);
for (int i = 0; i < count; i++) {
arb->data[arb->write_index] = samples[i];
}
// Check if jitter buffer filled
int available = (arb->write_index - arb->read_index + AUDIO_RING_BUFFER_SIZE)
arb->jitter_buffer_filled = true;
log_info("Jitter buffer filled, starting playback");
}
}
// Audio callback: Read samples for playback
void audio_callback(audio_ring_buffer_t *arb, float *output, int count) {
mutex_lock(&arb->mutex);
if (!arb->jitter_buffer_filled) {
// Not ready yet, output silence
memset(output, 0, count * sizeof(float));
return;
}
for (int i = 0; i < count; i++) {
output[i] = arb->data[arb->read_index];
}
}
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
#define AUDIO_JITTER_BUFFER_THRESHOLD
Jitter buffer threshold - samples needed before starting playback.
Definition ringbuffer.h:143

Note that the audio buffer uses a mutex. Unlike the generic ring buffer, this one can benefit from mutual exclusion since audio callbacks have strict timing requirements and we need to coordinate the jitter buffer logic.

API Reference

Now let's look at the actual API you'll be working with. The functions are straightforward and follow a consistent pattern.

Creation/Destruction

Creating a ring buffer is simple—just specify the element size and desired capacity:

// Create generic ring buffer (capacity rounded up to power of 2)
ringbuffer_t *ringbuffer_create(size_t element_size, size_t capacity);
// Create frame buffer
// Create multi-source frame buffer
// Destroy buffers (frees all memory)

Remember that the capacity gets rounded up to the next power of 2 for performance reasons. If you request 100 elements, you'll get 128. This usually isn't a problem, but it's good to be aware of.

Operations

The core operations are write, read, and peek. The query functions let you check the buffer state without modifying it.

// Generic ring buffer
bool ringbuffer_write(ringbuffer_t *rb, const void *data);
bool ringbuffer_read(ringbuffer_t *rb, void *data);
bool ringbuffer_peek(ringbuffer_t *rb, void *data); // Read without removing
// Query state
size_t ringbuffer_size(const ringbuffer_t *rb);
// Frame buffer
bool framebuffer_write_frame(framebuffer_t *fb, const char *data, size_t size);
// Multi-source frame buffer
bool framebuffer_write_multi_frame(framebuffer_t *fb, const char *data, size_t size,
uint32_t client_id, uint32_t seq, uint32_t ts);
void ringbuffer_clear(ringbuffer_t *rb)
Clear all elements from the buffer.
Definition ringbuffer.c:133
size_t ringbuffer_size(const ringbuffer_t *rb)
Get current number of elements in the buffer.
Definition ringbuffer.c:121
bool ringbuffer_is_full(const ringbuffer_t *rb)
Check if buffer is full.
Definition ringbuffer.c:129
bool ringbuffer_is_empty(const ringbuffer_t *rb)
Check if buffer is empty.
Definition ringbuffer.c:125
bool framebuffer_peek_latest_multi_frame(framebuffer_t *fb, multi_source_frame_t *frame)
Peek at the latest multi-source frame without removing it.
Definition ringbuffer.c:460
bool ringbuffer_peek(ringbuffer_t *rb, void *data)
Peek at the next element without removing it.
Definition ringbuffer.c:104

All the write and read functions return false if the operation couldn't complete (buffer full or empty). This lets you decide what to do—maybe drop the data, maybe wait and retry, maybe signal an error. It's up to you!

Performance

Let's talk numbers. The ring buffer isn't just fast—it's dramatically faster than traditional mutex-protected queues. Here's what the benchmarks show.

Benchmarks

We compared the lock-free ring buffer against a mutex-protected queue in a contention-free scenario (producer and consumer on the same CPU core):

Operation Ring Buffer Mutex Queue Speedup
Write 18 ns 95 ns 5.3x faster
Read 20 ns 102 ns 5.1x faster
Write+Read pair 38 ns 197 ns 5.2x faster

These numbers are from an AMD Ryzen 9 5950X in a contention-free scenario. The ring buffer operations are so fast they're barely more expensive than a simple memory copy!

Contention Behavior

What happens when things get more intense? We also tested with high contention, where the producer and consumer are running on different CPU cores. This stresses the cache coherency protocol, but the ring buffer still comes out way ahead:

Under high contention:

  • Ring buffer: ~45 ns per operation (2.4x slowdown from cache coherency)
  • Mutex queue: ~280 ns per operation (2.8x slowdown from lock contention)
  • Ring buffer still 6.2x faster under contention

Even when the CPU cores are fighting over cache lines, the ring buffer's lock-free design means it doesn't have to wait for mutexes to be released. The atomic operations are still much faster than lock acquisition.

Capacity Management

Getting the capacity right is important for performance. Too small and you'll drop data. Too large and you're wasting memory. The ring buffer helps by optimizing capacity calculations automatically.

Power-of-2 Optimization

The ring buffer always rounds capacity up to the next power of 2. This lets us use bitwise AND instead of modulo for index calculations, which is dramatically faster.

Here's how it works:

// Request 100 elements → gets rounded to 128 (next power of 2)
ringbuffer_t *rb = ringbuffer_create(sizeof(int), 100);
assert(rb->capacity == 128);
// Fast modulo using bitwise AND
size_t index = head & rb->capacity_mask; // Instead of head % capacity
// Where capacity_mask = capacity - 1 (e.g., 128 - 1 = 127 = 0b01111111)

The performance impact is significant:

  • Bitwise AND: 1 CPU cycle
  • Integer modulo: 15-30 CPU cycles
  • Speedup: 15-30x for index calculation

Since we're calculating the index on every read and write, this optimization really adds up. It's one of those small details that makes a big difference when you're doing thousands of operations per second.

Sizing Guidelines

Choosing the right buffer size depends on what you're buffering. Let's look at some common scenarios:

Video frames (30 FPS):

For video, you want enough frames to survive brief CPU spikes and scheduling delays. We recommend at least 2 seconds worth (60 frames) as a minimum. That gives you enough cushion that a momentary slowdown won't cause frame drops. For more robust operation, 4 seconds (120 frames) is better. Going beyond 10 seconds (300 frames) doesn't usually help much—by then you're just increasing latency.

Audio samples (48 kHz):

Audio buffering is a balancing act. You need enough buffer to absorb network jitter, but not so much that latency becomes noticeable. Our audio ring buffer uses:

  • Jitter buffer threshold: 46ms (2208 samples) - waits this long before starting playback
  • Total buffer: 186ms (8928 samples) - prevents underruns during playback

These numbers are tuned for typical network conditions. If you're on a very jittery network, you might want to increase the jitter buffer threshold. If latency is more important than smoothness, you can decrease it.

Thread Safety

This is important: ring buffers are only safe for the Single Producer, Single Consumer (SPSC) pattern. That means exactly one thread writing and exactly one thread reading. If you need multiple producers or consumers, you'll want to use packet_queue instead, which is mutex-protected.

SPSC Guarantee

The lock-free design relies on the fact that each position in the buffer is only accessed by one thread. The producer writes at head, the consumer reads from tail, and they never touch each other's positions. This is what makes the atomic operations sufficient—we don't need locks because there's no contention.

Here's the correct pattern:

// ✅ CORRECT: One producer, one consumer
void* producer_thread(void *arg) {
while (running) {
int data = generate_data();
ringbuffer_write(rb, &data);
}
}
void* consumer_thread(void *arg) {
while (running) {
int data;
if (ringbuffer_read(rb, &data)) {
process_data(data);
}
}
}

And here's what not to do:

// ❌ WRONG: Multiple producers (not thread-safe!)
void* producer_thread_1(void *arg) {
ringbuffer_write(rb, &data); // Race condition!
}
void* producer_thread_2(void *arg) {
ringbuffer_write(rb, &data); // Race condition!
}

If you try to have multiple producers writing at the same time, you'll get race conditions on the head pointer. Same problem with multiple consumers. The lock-free design only works when there's exactly one of each.

For multiple producers or consumers, use packet_queue instead—it uses mutexes and can handle multiple writers and readers safely, though it won't be as fast.

Best Practices

Here are some tips we've learned from using ring buffers in ascii-chat:

Use power-of-2 capacities when possible

While the ring buffer will round up for you, if you know your requirements in advance, it's cleaner to request a power of 2 directly. That way there's no surprise about the actual capacity you get.

ringbuffer_create(sizeof(int), 128); // Good (power of 2)
ringbuffer_create(sizeof(int), 100); // Still works, but rounded to 128

Handle full/empty cases gracefully

When ringbuffer_write() returns false, the buffer is full. When ringbuffer_read() returns false, it's empty. You need to decide what to do in each case:

if (!ringbuffer_write(rb, &data)) {
// Buffer full - decide policy:
// - Drop data (real-time video - newest frame is most important)
// - Wait and retry (critical data - can't afford to lose)
// - Overwrite oldest (circular log - keep latest N items)
}

For real-time video, dropping frames is usually fine—better to show the latest frame than to delay everything. For critical data, you might want to block until there's room. It depends on your use case.

Frame data is managed by buffer pool

Frame data is allocated from the buffer pool and automatically managed. Do NOT free frame data manually - the buffer pool handles cleanup when framebuffer_clear() or framebuffer_destroy() is called.

process_frame(frame.data, frame.size);
// DO NOT call SAFE_FREE(frame.data) - buffer pool owns it!
}

The buffer pool automatically reclaims frame memory when the buffer is cleared or destroyed. Calling SAFE_FREE() on frame data will cause double-free errors or corruption.

Validate magic numbers

Frame buffers include magic numbers for a reason—they help catch corruption. Always check that you got a valid frame:

if (frame.magic != FRAME_MAGIC) {
log_error("Corrupted frame detected (magic=0x%08X)", frame.magic);
return;
}
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587

This is a cheap sanity check that can save you from chasing weird bugs later. If you read garbage memory (maybe from a use-after-free or buffer overflow), the magic number won't match and you'll know immediately that something's wrong.

Size jitter buffers appropriately

For audio buffers, the jitter buffer size is a trade-off. Too small and you'll get audio crackling from underruns. Too large and you'll have noticeable latency. The sweet spot is usually about 2-3 times your typical network jitter.

Our default is 46 milliseconds, which works well for most network conditions. If you're on a very stable network (like a local connection), you could go smaller. If network jitter is severe, you might need to increase it, though you'll pay for it with latency.

See also
ringbuffer.h
ringbuffer.c

Macro Definition Documentation

◆ AUDIO_CROSSFADE_SAMPLES

#define AUDIO_CROSSFADE_SAMPLES   256

#include <ringbuffer.h>

Crossfade duration in samples for smooth underrun recovery.

When recovering from an underrun, we fade in the audio over this many samples to prevent clicks and pops. 256 samples @ 48kHz = ~5.3ms

Definition at line 186 of file ringbuffer.h.

◆ AUDIO_JITTER_BUFFER_THRESHOLD

#define AUDIO_JITTER_BUFFER_THRESHOLD   4800

#include <ringbuffer.h>

Jitter buffer threshold - samples needed before starting playback.

This threshold determines how much audio must be buffered before playback starts. Too small = underruns and audio gaps when network packets arrive late Too large = excessive latency + AEC3 gets silence during fill period

INCREASED from 960 (20ms) to 4800 (100ms) to eliminate stuttering caused by:

  • Network jitter (packets arriving in bursts)
  • Packet processing overhead
  • Thread scheduling delays

4800 samples @ 48kHz = 100ms = 5 Opus frames

Definition at line 143 of file ringbuffer.h.

◆ AUDIO_JITTER_HIGH_WATER_MARK

#define AUDIO_JITTER_HIGH_WATER_MARK   (AUDIO_JITTER_BUFFER_THRESHOLD * 2)

#include <ringbuffer.h>

High water mark - drop OLD samples when buffer exceeds this.

When the playback buffer grows beyond this level, we have accumulated too much latency. We drop OLD samples to keep latency bounded. This prevents the "latency grows over time" problem where network bursts fill the buffer faster than playback can drain it.

Set to 2x jitter threshold = 40ms for LAN. This gives enough headroom for network jitter while preventing unbounded latency accumulation. 1920 samples @ 48kHz = 40ms = 2 Opus frames

Definition at line 169 of file ringbuffer.h.

◆ AUDIO_JITTER_LOW_WATER_MARK

#define AUDIO_JITTER_LOW_WATER_MARK   2400

#include <ringbuffer.h>

Low water mark - warn when available drops below this.

When playback buffer drops below this threshold, we log a warning but keep playing to avoid choppy audio. The jitter buffer should absorb network jitter without constantly pausing/resuming.

INCREASED from 480 (10ms) to 2400 (50ms) to provide adequate headroom and prevent thousands of micro-underruns that cause audio stuttering.

2400 samples @ 48kHz = 50ms = 2.5 Opus frames

Definition at line 156 of file ringbuffer.h.

◆ AUDIO_JITTER_TARGET_LEVEL

#define AUDIO_JITTER_TARGET_LEVEL   (AUDIO_JITTER_BUFFER_THRESHOLD + 480)

#include <ringbuffer.h>

Target buffer level after dropping old samples.

When we drop old samples due to exceeding HIGH_WATER_MARK, we bring the buffer back down to this level. Set slightly above jitter threshold to avoid immediately triggering underrun detection.

1440 samples @ 48kHz = 30ms = 1.5 Opus frames

Definition at line 179 of file ringbuffer.h.

◆ AUDIO_RING_BUFFER_SIZE

#define AUDIO_RING_BUFFER_SIZE   192000

#include <ringbuffer.h>

Audio ring buffer size in samples (192000 samples = 4 seconds @ 48kHz)

Sized to handle:

  1. Single-client period where audio accumulates before a second client joins
  2. Mixer processing overhead (can take 3-15ms, exceeding 10ms target)
  3. System scheduling delays under load

4 seconds provides headroom for mixer slowdowns and prevents overflow during processing spikes. The mixer reads adaptively to catch up.

Definition at line 128 of file ringbuffer.h.

◆ FRAME_FREED

#define FRAME_FREED   MAGIC_FRAME_FREED

#include <ringbuffer.h>

Magic number indicating a freed frame (0xFEEDFACE) - Alias for backward compatibility.

Definition at line 424 of file ringbuffer.h.

◆ FRAME_MAGIC

#define FRAME_MAGIC   MAGIC_FRAME_VALID

#include <ringbuffer.h>

Magic number indicating a valid frame (0xDEADBEEF) - Alias for backward compatibility.

Definition at line 422 of file ringbuffer.h.

Typedef Documentation

◆ audio_ring_buffer_t

#include <ringbuffer.h>

Audio ring buffer for real-time audio streaming.

Specialized ring buffer for audio samples with jitter buffering to compensate for network latency and packet timing variations. Uses mutex-protected operations for thread-safe audio access.

Features:

  • Jitter buffering: waits for threshold fill before starting playback
  • Low water mark: resets jitter state when buffer runs low
  • Crossfade: smoothly fades audio in/out during underrun recovery
  • Underrun tracking: counts underruns for diagnostics

Function Documentation

◆ framebuffer_clear()

void framebuffer_clear ( framebuffer_t *  fb)

#include <ringbuffer.h>

Clear all frames from the buffer, freeing their data.

Parameters
fbFrame buffer (must not be NULL)

Removes all frames from the buffer and frees their associated data. This operation is thread-safe but should be used with caution if other threads may be reading frames.

Note
Thread-safe: Protected by mutex for concurrent access.

Definition at line 322 of file ringbuffer.c.

322 {
323 if (!fb || !fb->rb)
324 return;
325
326 // Thread-safe access to framebuffer (was missing, causing race conditions)
327 mutex_lock(&fb->mutex);
328
329 // Check the element size to determine frame type
330 if (fb->rb->element_size == sizeof(multi_source_frame_t)) {
331 // Multi-source frame buffer - read and free multi_source_frame_t
332 multi_source_frame_t multi_frame;
333 while (ringbuffer_read(fb->rb, &multi_frame)) {
334 if (IS_FRAME_VALID((frame_t *)&multi_frame)) {
335 MARK_FRAME_FREED((frame_t *)&multi_frame); // Mark as freed to detect use-after-free
336 buffer_pool_free(NULL, multi_frame.data, multi_frame.size);
337 } else if (!IS_MAGIC_VALID(multi_frame.magic, FRAME_MAGIC) && multi_frame.magic != 0) {
338 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid multi-source frame magic 0x%x during clear",
339 multi_frame.magic);
340 }
341 }
342 } else if (fb->rb->element_size == sizeof(frame_t)) {
343 // Single-source frame buffer - read and free frame_t
345 while (ringbuffer_read(fb->rb, &frame)) {
346 if (IS_FRAME_VALID(&frame)) {
347 MARK_FRAME_FREED(&frame); // Mark as freed to detect use-after-free
348 buffer_pool_free(NULL, frame.data, frame.size);
349 } else if (!IS_MAGIC_VALID(frame.magic, FRAME_MAGIC) && frame.magic != 0) {
350 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid frame magic 0x%x during clear", frame.magic);
351 }
352 }
353 } else {
354 SET_ERRNO(ERROR_INVALID_STATE, "Unknown frame buffer type with element size %zu", fb->rb->element_size);
355 }
356
357 // Clear the ringbuffer indices
358 ringbuffer_clear(fb->rb);
359
360 // Zero out the entire buffer to prevent any dangling pointers
361 if (fb->rb->buffer) {
362 // Check for integer overflow before multiplication
363 if (fb->rb->capacity > SIZE_MAX / fb->rb->element_size) {
364 SET_ERRNO(ERROR_INVALID_PARAM, "Buffer size would overflow: capacity=%zu, element_size=%zu", fb->rb->capacity,
365 fb->rb->element_size);
366 mutex_unlock(&fb->mutex);
367 return;
368 }
369 size_t buffer_size = fb->rb->capacity * fb->rb->element_size;
371 }
372
373 mutex_unlock(&fb->mutex);
374}
int buffer_size
Size of circular buffer.
Definition grep.c:90
void buffer_pool_free(buffer_pool_t *pool, const void *data, size_t size)
Free a buffer back to the pool (lock-free)
#define SAFE_MEMSET(dest, dest_size, ch, count)
Definition common.h:469
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
@ ERROR_INVALID_STATE
@ ERROR_INVALID_PARAM
#define IS_MAGIC_VALID(magic, expected)
Check if magic number is valid (true if matches expected)
Definition magic.h:36
#define IS_FRAME_VALID(frame)
Check if frame is valid and not freed.
Definition magic.h:39
#define MARK_FRAME_FREED(frame)
Mark a frame as freed.
Definition magic.h:49
char * data
Pointer to frame data (not owned by this struct)
Definition ringbuffer.h:389
uint64_t magic
Magic number to detect corruption (FRAME_MAGIC when valid)
Definition ringbuffer.h:385
size_t size
Actual size of frame data in bytes.
Definition ringbuffer.h:387
mutex_t mutex
Mutex for thread-safe access to framebuffer operations.
Definition ringbuffer.h:440
ringbuffer_t * rb
Underlying ring buffer for frame storage.
Definition ringbuffer.h:438

References ringbuffer_t::buffer, buffer_pool_free(), buffer_size, ringbuffer_t::capacity, frame_t::data, ringbuffer_t::element_size, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, frame, FRAME_MAGIC, IS_FRAME_VALID, IS_MAGIC_VALID, frame_t::magic, MARK_FRAME_FREED, framebuffer_t::mutex, mutex_lock, mutex_unlock, framebuffer_t::rb, ringbuffer_clear(), ringbuffer_read(), SAFE_MEMSET, SET_ERRNO, and frame_t::size.

Referenced by framebuffer_destroy().

◆ framebuffer_create()

framebuffer_t * framebuffer_create ( size_t  capacity)

#include <ringbuffer.h>

Create a frame buffer for ASCII frames.

Parameters
capacityNumber of frames to buffer
Returns
Frame buffer or NULL on failure

Creates a new frame buffer for single-source frame scenarios. The capacity is automatically rounded up to the next power of 2 for optimal performance.

Definition at line 146 of file ringbuffer.c.

146 {
147 if (capacity == 0) {
148 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid frame buffer parameters");
149 return NULL;
150 }
151
152 framebuffer_t *fb;
153 fb = SAFE_CALLOC(1, sizeof(framebuffer_t), framebuffer_t *);
154
155 // Initialize mutex for thread-safe access
156 if (mutex_init(&fb->mutex, "framebuffer") != 0) {
157 SET_ERRNO(ERROR_THREAD, "Failed to initialize framebuffer mutex");
158 SAFE_FREE(fb);
159 return NULL;
160 }
161
162 // Create ringbuffer to store frame_t structs
163 fb->rb = ringbuffer_create(sizeof(frame_t), capacity);
164 if (!fb->rb) {
165 SET_ERRNO(ERROR_MEMORY, "Failed to allocate frame buffer");
166 mutex_destroy(&fb->mutex);
167 SAFE_FREE(fb);
168 return NULL;
169 }
170
171 return fb;
172}
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
@ ERROR_MEMORY
Definition error_codes.h:56
@ ERROR_THREAD
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22

References ERROR_INVALID_PARAM, ERROR_MEMORY, ERROR_THREAD, framebuffer_t::mutex, mutex_destroy(), mutex_init(), framebuffer_t::rb, ringbuffer_create(), SAFE_CALLOC, SAFE_FREE, and SET_ERRNO.

◆ framebuffer_create_multi()

framebuffer_t * framebuffer_create_multi ( size_t  capacity)

#include <ringbuffer.h>

Create a multi-source frame buffer for multi-user support.

Parameters
capacityNumber of frames to buffer
Returns
Frame buffer or NULL on failure

Creates a new frame buffer for multi-client frame scenarios. Supports tracking frame sources, sequence numbers, and timestamps for proper multi-user frame display.

Definition at line 174 of file ringbuffer.c.

174 {
175 if (capacity == 0) {
176 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid capacity: %zu", capacity);
177 return NULL;
178 }
179
180 framebuffer_t *fb;
181 fb = SAFE_CALLOC(1, sizeof(framebuffer_t), framebuffer_t *);
182
183 // Initialize mutex for thread-safe access
184 if (mutex_init(&fb->mutex, "framebuffer_multi") != 0) {
185 SET_ERRNO(ERROR_THREAD, "Failed to initialize framebuffer mutex");
186 SAFE_FREE(fb);
187 return NULL;
188 }
189
190 // Create ringbuffer to store multi_source_frame_t structs
191 fb->rb = ringbuffer_create(sizeof(multi_source_frame_t), capacity);
192
193 if (!fb->rb) {
194 mutex_destroy(&fb->mutex);
195 SAFE_FREE(fb);
196 return NULL;
197 }
198
199 return fb;
200}

References ERROR_INVALID_PARAM, ERROR_THREAD, framebuffer_t::mutex, mutex_destroy(), mutex_init(), framebuffer_t::rb, ringbuffer_create(), SAFE_CALLOC, SAFE_FREE, and SET_ERRNO.

◆ framebuffer_destroy()

void framebuffer_destroy ( framebuffer_t *  fb)

#include <ringbuffer.h>

Destroy a frame buffer.

Parameters
fbFrame buffer to destroy (must not be NULL)

Frees all memory associated with the frame buffer, including any frames still in the buffer. The buffer must not be in use by any threads when this function is called.

Definition at line 202 of file ringbuffer.c.

202 {
203 if (!fb)
204 return;
205
206 // Add magic number check to detect double-free using rb pointer
207 if (fb->rb == (ringbuffer_t *)0xDEADBEEF) {
208 SET_ERRNO(ERROR_INVALID_STATE, "DOUBLE-FREE DETECTED: framebuffer %p already destroyed!", fb);
209 return;
210 }
211
214 mutex_destroy(&fb->mutex);
215
216 // Mark as destroyed before freeing
217 fb->rb = (ringbuffer_t *)0xDEADBEEF;
218 SAFE_FREE(fb);
219}
void framebuffer_clear(framebuffer_t *fb)
Clear all frames from the buffer, freeing their data.
Definition ringbuffer.c:322

References ERROR_INVALID_STATE, framebuffer_clear(), framebuffer_t::mutex, mutex_destroy(), framebuffer_t::rb, ringbuffer_destroy(), SAFE_FREE, and SET_ERRNO.

◆ framebuffer_peek_latest_multi_frame()

bool framebuffer_peek_latest_multi_frame ( framebuffer_t *  fb,
multi_source_frame_t *  frame 
)

#include <ringbuffer.h>

Peek at the latest multi-source frame without removing it.

Parameters
fbFrame buffer (must not be NULL)
framePointer to multi_source_frame_t struct to be filled (must not be NULL)
Returns
true if successful, false if buffer is empty

Examines the latest multi-source frame without removing it from the buffer. Useful for previewing frames or checking frame metadata before reading.

Note
Thread-safe: Protected by mutex for concurrent access.
The frame data pointer is valid only until the buffer is modified. Do not use the data pointer after subsequent buffer operations.

Definition at line 460 of file ringbuffer.c.

460 {
461 if (!fb || !fb->rb || !frame) {
462 return false;
463 }
464
465 // Thread-safe access to framebuffer
466 mutex_lock(&fb->mutex);
467
468 // Use ringbuffer_peek to get the frame without consuming it
469 bool result = ringbuffer_peek(fb->rb, frame);
470
471 if (result) {
472 // Validate frame magic
473 if (!IS_MAGIC_VALID(frame->magic, FRAME_MAGIC)) {
474 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid multi-source frame magic 0x%x (expected 0x%x) in peek",
475 frame->magic, FRAME_MAGIC);
476 frame->data = NULL;
477 frame->size = 0;
478 memset(frame->source_client_id, 0, sizeof(frame->source_client_id));
479 mutex_unlock(&fb->mutex);
480 return false;
481 }
482
483 // Additional validation
484 if (frame->size == 0 || !frame->data) {
485 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid multi-source frame data (size=%zu, data=%p) in peek",
486 frame->size, frame->data);
487 mutex_unlock(&fb->mutex);
488 return false;
489 }
490
491 // IMPORTANT: We need to make a copy of the data since we're not consuming the frame
492 // The original data pointer will remain valid in the ring buffer
493 // Caller is responsible for freeing this copy with buffer_pool_free()
494 char *data_copy;
495 data_copy = (char *)buffer_pool_alloc(NULL, frame->size);
496 if (!data_copy) {
497 SET_ERRNO(ERROR_MEMORY, "Failed to allocate memory for frame data copy in peek");
498 mutex_unlock(&fb->mutex);
499 return false;
500 }
501 SAFE_MEMCPY(data_copy, frame->size, frame->data, frame->size);
502 frame->data = data_copy;
503 }
504
505 mutex_unlock(&fb->mutex);
506 return result;
507}
void * buffer_pool_alloc(buffer_pool_t *pool, size_t size)
Allocate a buffer from the pool (lock-free fast path)
#define SAFE_MEMCPY(dest, dest_size, src, count)
Definition common.h:468

References buffer_pool_alloc(), ERROR_INVALID_STATE, ERROR_MEMORY, frame, FRAME_MAGIC, IS_MAGIC_VALID, framebuffer_t::mutex, mutex_lock, mutex_unlock, framebuffer_t::rb, ringbuffer_peek(), SAFE_MEMCPY, and SET_ERRNO.

◆ framebuffer_read_frame()

bool framebuffer_read_frame ( framebuffer_t *  fb,
frame_t *  frame 
)

#include <ringbuffer.h>

Read a frame from the buffer.

Parameters
fbFrame buffer (must not be NULL)
framePointer to frame_t struct to be filled (must not be NULL)
Returns
true if successful, false if buffer is empty

Reads a single-source frame from the buffer. The frame data pointer points to data owned by the buffer until the frame is cleared or the buffer is destroyed.

Note
Thread-safe: Protected by mutex for concurrent access.
The caller must not free the frame data pointer - it's owned by the buffer until the frame is consumed.

Definition at line 275 of file ringbuffer.c.

275 {
276 if (!fb || !frame) {
277 return false;
278 }
279
280 // Initialize frame to safe values
281 frame->magic = 0;
282 frame->data = NULL;
283 frame->size = 0;
284
285 // Thread-safe access to framebuffer (fixes TOCTOU race condition)
286 mutex_lock(&fb->mutex);
287
288 bool result = ringbuffer_read(fb->rb, frame);
289
290 // Validate the frame we just read
291 if (result) {
292 if (!IS_MAGIC_VALID(frame->magic, FRAME_MAGIC)) {
293 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid frame magic 0x%x (expected 0x%x)", frame->magic, FRAME_MAGIC);
294 frame->data = NULL;
295 frame->size = 0;
296 mutex_unlock(&fb->mutex);
297 return false;
298 }
299
300 if (IS_FRAME_FREED(frame)) {
301 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Reading already-freed frame!");
302 frame->data = NULL;
303 frame->size = 0;
304 mutex_unlock(&fb->mutex);
305 return false;
306 }
307
308 if (frame->size > 10 * 1024 * 1024) {
309 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Frame size too large: %zu", frame->size);
310 SAFE_FREE(frame->data);
311 frame->data = NULL;
312 frame->size = 0;
313 mutex_unlock(&fb->mutex);
314 return false;
315 }
316 }
317
318 mutex_unlock(&fb->mutex);
319 return result;
320}
#define IS_FRAME_FREED(frame)
Check if frame has been freed (corruption detection)
Definition magic.h:42

References ERROR_INVALID_STATE, frame, FRAME_MAGIC, IS_FRAME_FREED, IS_MAGIC_VALID, framebuffer_t::mutex, mutex_lock, mutex_unlock, framebuffer_t::rb, ringbuffer_read(), SAFE_FREE, and SET_ERRNO.

◆ framebuffer_read_multi_frame()

bool framebuffer_read_multi_frame ( framebuffer_t *  fb,
multi_source_frame_t *  frame 
)

#include <ringbuffer.h>

Read a multi-source frame from the buffer.

Parameters
fbFrame buffer (must not be NULL)
framePointer to multi_source_frame_t struct to be filled (must not be NULL)
Returns
true if successful, false if buffer is empty

Reads a multi-source frame from the buffer along with its metadata. The frame data pointer points to data owned by the buffer until the frame is cleared or the buffer is destroyed.

Note
Thread-safe: Protected by mutex for concurrent access.
The caller must not free the frame data pointer - it's owned by the buffer until the frame is consumed.

Definition at line 425 of file ringbuffer.c.

425 {
426 if (!fb || !fb->rb || !frame) {
427 return false;
428 }
429
430 // Thread-safe access to framebuffer
431 mutex_lock(&fb->mutex);
432
433 bool result = ringbuffer_read(fb->rb, frame);
434
435 if (result) {
436 // Validate frame magic
437 if (!IS_MAGIC_VALID(frame->magic, FRAME_MAGIC)) {
438 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid multi-source frame magic 0x%x (expected 0x%x)", frame->magic,
440 frame->data = NULL;
441 frame->size = 0;
442 memset(frame->source_client_id, 0, sizeof(frame->source_client_id));
443 mutex_unlock(&fb->mutex);
444 return false;
445 }
446
447 // Additional validation
448 if (frame->size == 0 || !frame->data) {
449 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid multi-source frame data (size=%zu, data=%p)", frame->size,
450 frame->data);
451 mutex_unlock(&fb->mutex);
452 return false;
453 }
454 }
455
456 mutex_unlock(&fb->mutex);
457 return result;
458}

References ERROR_INVALID_STATE, frame, FRAME_MAGIC, IS_MAGIC_VALID, framebuffer_t::mutex, mutex_lock, mutex_unlock, framebuffer_t::rb, ringbuffer_read(), and SET_ERRNO.

◆ framebuffer_write_frame()

bool framebuffer_write_frame ( framebuffer_t *  fb,
const char *  frame_data,
size_t  frame_size 
)

#include <ringbuffer.h>

Write a frame to the buffer.

Parameters
fbFrame buffer (must not be NULL)
frame_dataFrame data (must not be NULL)
frame_sizeActual size of frame data in bytes
Returns
true if successful, false if buffer is full

Writes a single-source frame to the buffer. The frame data is copied into the buffer, so the caller retains ownership of the original data.

Note
Thread-safe: Protected by mutex for concurrent access.

Definition at line 221 of file ringbuffer.c.

221 {
222 if (!fb || !frame_data || frame_size == 0)
223 return false;
224
225 // Validate frame size to prevent overflow
226 if (frame_size > 10 * 1024 * 1024) { // 10MB max for ASCII frames
227 SET_ERRNO(ERROR_INVALID_PARAM, "Rejecting oversized frame: %zu bytes", frame_size);
228 return false;
229 }
230
231 // Thread-safe access to framebuffer (was missing, causing race conditions)
232 mutex_lock(&fb->mutex);
233
234 // Check if buffer is full - if so, we need to drop the oldest frame
235 if (ringbuffer_size(fb->rb) >= fb->rb->capacity) {
236 // Buffer is full, read and free the oldest frame before writing new one
237 frame_t old_frame;
238 if (ringbuffer_read(fb->rb, &old_frame)) {
239 if (IS_FRAME_VALID(&old_frame)) {
240 MARK_FRAME_FREED(&old_frame);
241 // Use buffer_pool_free since data was allocated with buffer_pool_alloc
242 buffer_pool_free(NULL, old_frame.data, old_frame.size);
243 } else if (!IS_MAGIC_VALID(old_frame.magic, FRAME_MAGIC)) {
244 SET_ERRNO(ERROR_INVALID_STATE, "CORRUPTION: Invalid old frame magic 0x%x when dropping", old_frame.magic);
245 }
246 }
247 }
248
249 // Allocate a copy of the frame data using buffer pool for better performance
250 char *frame_copy = (char *)buffer_pool_alloc(NULL, frame_size + 1);
251 if (!frame_copy) {
252 mutex_unlock(&fb->mutex);
253 SET_ERRNO(ERROR_MEMORY, "Failed to allocate %zu bytes from buffer pool for frame", frame_size + 1);
254 return false;
255 }
256
257 SAFE_MEMCPY(frame_copy, frame_size, frame_data, frame_size);
258 frame_copy[frame_size] = '\0'; // Ensure null termination
259
260 // Create a frame_t struct with the copy (store allocated size for proper cleanup)
261 frame_t frame = {.magic = MAGIC_FRAME_VALID, .size = frame_size + 1, .data = frame_copy};
262
263 bool result = ringbuffer_write(fb->rb, &frame);
264
265 if (!result) {
266 // If we still couldn't write to ringbuffer, return the buffer to pool
267 buffer_pool_free(NULL, frame_copy, frame_size + 1);
268 SET_ERRNO(ERROR_INVALID_STATE, "Failed to write frame to ringbuffer even after dropping oldest");
269 }
270
271 mutex_unlock(&fb->mutex);
272 return result;
273}
#define MAGIC_FRAME_VALID
Magic number for valid ring buffer frames (0xA5C11C4A1 = "ASCIICHAT" in hex)
Definition magic.h:17

References buffer_pool_alloc(), buffer_pool_free(), ringbuffer_t::capacity, frame_t::data, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, ERROR_MEMORY, frame, FRAME_MAGIC, IS_FRAME_VALID, IS_MAGIC_VALID, frame_t::magic, MAGIC_FRAME_VALID, MARK_FRAME_FREED, framebuffer_t::mutex, mutex_lock, mutex_unlock, framebuffer_t::rb, ringbuffer_read(), ringbuffer_size(), ringbuffer_write(), SAFE_MEMCPY, SET_ERRNO, and frame_t::size.

◆ framebuffer_write_multi_frame()

bool framebuffer_write_multi_frame ( framebuffer_t *  fb,
const char *  frame_data,
size_t  frame_size,
uint32_t  source_client_id,
uint32_t  frame_sequence,
uint32_t  timestamp 
)

#include <ringbuffer.h>

Write a multi-source frame to the buffer (for multi-user support)

Parameters
fbFrame buffer (must not be NULL)
frame_dataFrame data (must not be NULL)
frame_sizeActual size of frame data in bytes
source_client_idClient ID that sent this frame
frame_sequenceFrame sequence number for ordering
timestampTimestamp when frame was captured
Returns
true if successful, false if buffer is full

Writes a multi-source frame to the buffer with associated metadata. The frame data is copied into the buffer, so the caller retains ownership of the original data.

Note
Thread-safe: Protected by mutex for concurrent access.
The frame buffer must have been created with framebuffer_create_multi() for multi-source support.

Definition at line 378 of file ringbuffer.c.

379 {
380 if (!fb || !fb->rb || !frame_data || frame_size == 0) {
381 return false;
382 }
383
384 // Allocate memory for frame data using buffer pool for better performance
385 char *data_copy = (char *)buffer_pool_alloc(NULL, frame_size);
386 if (!data_copy) {
387 SET_ERRNO(ERROR_MEMORY, "Failed to allocate %zu bytes from buffer pool for multi-source frame", frame_size);
388 return false;
389 }
390
391 // Copy frame data
392 SAFE_MEMCPY(data_copy, frame_size, frame_data, frame_size);
393
394 // Convert client ID number to string for storage in frame metadata
395 char client_id_str[64];
396 snprintf(client_id_str, sizeof(client_id_str), "%u", source_client_id);
397
398 // Create multi-source frame
399 multi_source_frame_t multi_frame = {
401 .source_client_id = {0},
402 .frame_sequence = frame_sequence,
403 .timestamp = timestamp,
404 .size = frame_size,
405 .data = data_copy,
406 };
407 SAFE_MEMCPY(multi_frame.source_client_id, sizeof(multi_frame.source_client_id), client_id_str,
408 strlen(client_id_str) + 1);
409
410 // Thread-safe access to framebuffer
411 mutex_lock(&fb->mutex);
412
413 // Try to write to ring buffer
414 bool success = ringbuffer_write(fb->rb, &multi_frame);
415 if (!success) {
416 // Buffer full, return the buffer to pool
417 buffer_pool_free(NULL, data_copy, frame_size);
418 log_debug("Frame buffer full, dropping multi-source frame from client %u", source_client_id);
419 }
420
421 mutex_unlock(&fb->mutex);
422 return success;
423}
uint64_t magic
Magic number to detect corruption (FRAME_MAGIC when valid)
Definition ringbuffer.h:402
char source_client_id[64]
Client ID that sent this frame.
Definition ringbuffer.h:404

References buffer_pool_alloc(), buffer_pool_free(), ERROR_MEMORY, log_debug, multi_source_frame_t::magic, MAGIC_FRAME_VALID, framebuffer_t::mutex, mutex_lock, mutex_unlock, framebuffer_t::rb, ringbuffer_write(), SAFE_MEMCPY, SET_ERRNO, and multi_source_frame_t::source_client_id.

◆ ringbuffer_clear()

void ringbuffer_clear ( ringbuffer_t *  rb)

#include <ringbuffer.h>

Clear all elements from the buffer.

Parameters
rbRing buffer (must not be NULL)

Removes all elements from the buffer and resets it to empty state. This operation is not atomic with respect to concurrent read/write operations - use with caution in multi-threaded scenarios.

Note
Thread-safe: Uses atomic operations, but concurrent operations may still occur during clearing.

Definition at line 133 of file ringbuffer.c.

133 {
134 if (rb) {
135 atomic_store_u64(&rb->head, 0);
136 atomic_store_u64(&rb->tail, 0);
137 atomic_store_u64(&rb->size, 0);
138 }
139}
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241

References atomic_store_u64(), ringbuffer_t::head, ringbuffer_t::size, and ringbuffer_t::tail.

Referenced by framebuffer_clear().

◆ ringbuffer_create()

ringbuffer_t * ringbuffer_create ( size_t  element_size,
size_t  capacity 
)

#include <ringbuffer.h>

Create a new ring buffer.

Parameters
element_sizeSize of each element in bytes
capacityNumber of elements (will be rounded up to power of 2)
Returns
Pointer to ring buffer or NULL on failure

Creates a new lock-free ring buffer with the specified element size and capacity. The capacity is automatically rounded up to the next power of 2 to enable fast bit masking optimizations.

Definition at line 29 of file ringbuffer.c.

29 {
30 if (element_size == 0 || capacity == 0) {
31 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid ring buffer parameters: element_size=%zu, capacity=%zu", element_size,
32 capacity);
33 return NULL;
34 }
35
36 ringbuffer_t *rb;
37 rb = SAFE_CALLOC(1, sizeof(ringbuffer_t), ringbuffer_t *);
38
39 /* Round capacity up to power of 2 for optimization */
40 size_t actual_capacity = math_next_power_of_two(capacity);
41
42 rb->buffer = SAFE_CALLOC(actual_capacity, element_size, char *);
43
44 rb->element_size = element_size;
45 rb->capacity = actual_capacity;
46 rb->is_power_of_two = true;
47 rb->capacity_mask = actual_capacity - 1;
48 /* Atomic fields are zero-initialized by SAFE_CALLOC */
49
50 return rb;
51}
bool is_power_of_two
Whether capacity is power of 2 (enables bit masking optimization)
Definition ringbuffer.h:102

References ringbuffer_t::buffer, ringbuffer_t::capacity, ringbuffer_t::capacity_mask, ringbuffer_t::element_size, ERROR_INVALID_PARAM, ringbuffer_t::is_power_of_two, SAFE_CALLOC, and SET_ERRNO.

Referenced by acip_webrtc_transport_create(), acip_websocket_client_transport_create(), acip_websocket_server_transport_create(), framebuffer_create(), framebuffer_create_multi(), session_host_add_client(), and session_host_add_memory_participant().

◆ ringbuffer_destroy()

void ringbuffer_destroy ( ringbuffer_t *  rb)

#include <ringbuffer.h>

Destroy a ring buffer and free its memory.

Parameters
rbRing buffer to destroy (must not be NULL)

Frees all memory associated with the ring buffer. The buffer must not be in use by any threads when this function is called.

Definition at line 53 of file ringbuffer.c.

53 {
54 if (rb) {
55 SAFE_FREE(rb->buffer);
56 SAFE_FREE(rb);
57 }
58}

References ringbuffer_t::buffer, and SAFE_FREE.

Referenced by acip_webrtc_transport_create(), acip_websocket_client_transport_create(), acip_websocket_server_transport_create(), framebuffer_destroy(), session_host_add_client(), session_host_add_memory_participant(), session_host_destroy(), and session_host_remove_client().

◆ ringbuffer_is_empty()

bool ringbuffer_is_empty ( const ringbuffer_t *  rb)

#include <ringbuffer.h>

Check if buffer is empty.

Parameters
rbRing buffer (must not be NULL)
Returns
true if buffer is empty, false otherwise

Performs a fast check to determine if the buffer contains any elements. More efficient than checking if size() == 0.

Note
Thread-safe: Uses atomic operations for lock-free check.

Definition at line 125 of file ringbuffer.c.

125 {
126 return ringbuffer_size(rb) == 0;
127}

References ringbuffer_size().

◆ ringbuffer_is_full()

bool ringbuffer_is_full ( const ringbuffer_t *  rb)

#include <ringbuffer.h>

Check if buffer is full.

Parameters
rbRing buffer (must not be NULL)
Returns
true if buffer is full, false otherwise

Performs a fast check to determine if the buffer has reached capacity. More efficient than checking if size() == capacity.

Note
Thread-safe: Uses atomic operations for lock-free check.

Definition at line 129 of file ringbuffer.c.

129 {
130 return rb ? ringbuffer_size(rb) >= rb->capacity : true;
131}

References ringbuffer_t::capacity, and ringbuffer_size().

◆ ringbuffer_peek()

bool ringbuffer_peek ( ringbuffer_t *  rb,
void *  data 
)

#include <ringbuffer.h>

Peek at the next element without removing it.

Parameters
rbRing buffer (must not be NULL)
dataBuffer to peek into (must not be NULL, must be element_size bytes)
Returns
true if successful, false if buffer is empty

Examines the next element without removing it from the buffer. This is useful for previewing data before committing to reading it.

Note
SPSC only: Single Producer, Single Consumer. NOT thread-safe for multiple concurrent peek operations.

Definition at line 104 of file ringbuffer.c.

104 {
105 if (!rb || !data)
106 return false;
107
108 size_t current_size = atomic_load_u64(&rb->size);
109 if (current_size == 0) {
110 return false; /* Buffer empty */
111 }
112
113 size_t tail = atomic_load_u64(&rb->tail);
114
115 /* Copy data without updating tail */
116 SAFE_MEMCPY(data, rb->element_size, rb->buffer + (tail * rb->element_size), rb->element_size);
117
118 return true;
119}
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233

References atomic_load_u64(), ringbuffer_t::buffer, ringbuffer_t::element_size, SAFE_MEMCPY, ringbuffer_t::size, and ringbuffer_t::tail.

Referenced by framebuffer_peek_latest_multi_frame().

◆ ringbuffer_read()

bool ringbuffer_read ( ringbuffer_t *  rb,
void *  data 
)

#include <ringbuffer.h>

Try to read an element from the ring buffer (non-blocking)

Parameters
rbRing buffer (must not be NULL)
dataBuffer to read into (must not be NULL, must be element_size bytes)
Returns
true if successful, false if buffer is empty

Attempts to read an element from the ring buffer. This is a non-blocking operation that uses lock-free atomic operations. Returns false immediately if the buffer is empty.

Note
SPSC only: Single Producer, Single Consumer. NOT thread-safe for multiple consumers - use external synchronization if needed.

Definition at line 82 of file ringbuffer.c.

82 {
83 if (!rb || !data)
84 return false;
85
86 size_t current_size = atomic_load_u64(&rb->size);
87 if (current_size == 0) {
88 return false; /* Buffer empty */
89 }
90
91 size_t tail = atomic_load_u64(&rb->tail);
92 size_t next_tail = (tail + 1) & rb->capacity_mask;
93
94 /* Copy data */
95 SAFE_MEMCPY(data, rb->element_size, rb->buffer + (tail * rb->element_size), rb->element_size);
96
97 /* Update tail and size atomically */
98 atomic_store_u64(&rb->tail, next_tail);
100
101 return true;
102}
uint64_t atomic_fetch_sub_u64(atomic_t *a, uint64_t delta)
Atomically subtract from a uint64_t and return the previous value.
Definition atomic.c:256

References atomic_fetch_sub_u64(), atomic_load_u64(), atomic_store_u64(), ringbuffer_t::buffer, ringbuffer_t::capacity_mask, ringbuffer_t::element_size, SAFE_MEMCPY, ringbuffer_t::size, and ringbuffer_t::tail.

Referenced by framebuffer_clear(), framebuffer_read_frame(), framebuffer_read_multi_frame(), and framebuffer_write_frame().

◆ ringbuffer_size()

size_t ringbuffer_size ( const ringbuffer_t *  rb)

#include <ringbuffer.h>

Get current number of elements in the buffer.

Parameters
rbRing buffer (must not be NULL)
Returns
Number of elements currently in the buffer

Returns the current size of the buffer. This is a snapshot value that may change immediately after return due to concurrent operations.

Note
Thread-safe: Uses atomic operations for lock-free size check.

Definition at line 121 of file ringbuffer.c.

121 {
122 return rb ? atomic_load_u64(&rb->size) : 0;
123}

References atomic_load_u64(), and ringbuffer_t::size.

Referenced by framebuffer_write_frame(), ringbuffer_is_empty(), and ringbuffer_is_full().

◆ ringbuffer_write()

bool ringbuffer_write ( ringbuffer_t *  rb,
const void *  data 
)

#include <ringbuffer.h>

Try to write an element to the ring buffer (non-blocking)

Parameters
rbRing buffer (must not be NULL)
dataData to write (must not be NULL, must be element_size bytes)
Returns
true if successful, false if buffer is full

Attempts to write an element to the ring buffer. This is a non-blocking operation that uses lock-free atomic operations. Returns false immediately if the buffer is full.

Note
SPSC only: Single Producer, Single Consumer. NOT thread-safe for multiple producers - use external synchronization if needed.

Definition at line 60 of file ringbuffer.c.

60 {
61 if (!rb || !data)
62 return false;
63
64 size_t current_size = atomic_load_u64(&rb->size);
65 if (current_size >= rb->capacity) {
66 return false; /* Buffer full */
67 }
68
69 size_t head = atomic_load_u64(&rb->head);
70 size_t next_head = (head + 1) & rb->capacity_mask;
71
72 /* Copy data */
73 SAFE_MEMCPY(rb->buffer + (head * rb->element_size), rb->element_size, data, rb->element_size);
74
75 /* Update head and size atomically */
76 atomic_store_u64(&rb->head, next_head);
78
79 return true;
80}
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248

References atomic_fetch_add_u64(), atomic_load_u64(), atomic_store_u64(), ringbuffer_t::buffer, ringbuffer_t::capacity, ringbuffer_t::capacity_mask, ringbuffer_t::element_size, ringbuffer_t::head, SAFE_MEMCPY, and ringbuffer_t::size.

Referenced by framebuffer_write_frame(), framebuffer_write_multi_frame(), and session_host_inject_audio().