1. Fixed-Capacity Ring Buffer for Streaming Audio Chunks
Problem
A real-time voice pipeline can't let its audio buffer grow without bound while audio keeps streaming in — memory has to stay flat no matter how long the call runs. The standard fix is a fixed-capacity ring (circular) buffer: push new chunks in, and once it's full, pushing a new chunk silently evicts the oldest one still waiting.
Implement the ring buffer's core push/pop mechanics using a fixed-size array and wraparound indices, not a Python list that resizes. Every operation must run in O(1).
Source: src/1_ring_buffer_audio_chunks.py
def run_ring_buffer(capacity: int, ops: list[tuple[str, int | None]]) -> list[int | None]:
...
Examples:
>>> run_ring_buffer(2, [("push", 1), ("push", 2), ("push", 3), ("pop", None)])
[None, None, 1, 2]
>>> run_ring_buffer(1, [("push", 5), ("pop", None), ("pop", None)])
[None, 5, None]
Step-by-Step Approach
- Back the buffer with a fixed-size array of length
capacity, plus aheadindex (oldest live element) and acountof how many slots are currently occupied — never resize the array. - On push: if
count < capacity, write into the next free slot at(head + count) % capacityand incrementcount; the push doesn't evict anything, so returnNone. - On push when
count == capacity: the buffer is full, so overwrite the slot athead, advanceheadby one (mod capacity), and return the value that was evicted. - On pop: if
count == 0returnNone; otherwise read the value athead, advanceheadby one (mod capacity), decrementcount, and return that value. - The modulo arithmetic is what gives wraparound — once
heador the write index reaches the end of the array it should cycle back to index 0 rather than growing the array. - Process every op in order, collecting one result per op, and return the full results list.
The key insight is that head and count alone are enough state to treat a fixed array as a circular queue — you never shift elements or resize, so every push and pop is O(1) regardless of how long the stream runs.
Reference solution
def run_ring_buffer(capacity: int, ops: list[tuple[str, int | None]]) -> list[int | None]:
# fixed-size backing array; head is the index of the oldest live element
buf: list[int | None] = [None] * capacity
head = 0
count = 0
results: list[int | None] = []
for kind, value in ops:
if kind == "push":
if count == capacity:
# full: overwrite the oldest slot and return what it held
evicted = buf[head]
buf[head] = value
head = (head + 1) % capacity
results.append(evicted)
else:
# not full: write into the next free slot after the tail
tail = (head + count) % capacity
buf[tail] = value
count += 1
results.append(None)
else: # "pop"
if count == 0:
results.append(None)
else:
results.append(buf[head])
head = (head + 1) % capacity
count -= 1
return results
Key Functions & Tricks
(head + count) % capacity— computes the next free write slot without a separate tail pointer.head = (head + 1) % capacity— the wraparound step that makes a fixed array behave as a circular queue.- head + count instead of head + tail — tracking a count alongside head disambiguates "empty" from "full" (both would otherwise look like head == tail).
count == capacitycheck — the single branch that decides whether a push is a plain write or an evict-and-overwrite.- Fixed-size list preallocation (
[None] * capacity) — guarantees O(1) writes with no resizing/copying ever occurring.
How to Recognize This Pattern
The signal is "bounded memory that must hold the most recent N items, oldest data yielding to newest" — anywhere a stream keeps producing but only a fixed recent window matters. A circular array with head/count (or head/tail) indices is the standard O(1)-per-op answer, versus a naive list where popping from the front is O(n). Common variations ask for the buffer's current contents at any point (walk from head for count entries) or for the buffer to overwrite silently versus reject the write when full (an explicit "is_full" check instead of evicting). A common pitfall is reaching for collections.deque with no maxlen and manually trimming it, which works but misses the point of a genuinely fixed-size, no-allocation structure; another is forgetting the modulo on index arithmetic and letting indices walk off the end of the array instead of wrapping.