Skip to content

Add support for batch reading frames #2

@VictorQueiroz

Description

@VictorQueiroz

🚀 Add Support for Batch Reading Frames from the Ring Buffer

📌 Summary

This functionality would give us additional leverage over memory management, as we would know ahead of time how many frames the user intends to read.

Instead of repeatedly calling .read() in a loop (which is the only available way to read multiple frames today), we could read a batch at once by slicing the underlying buffer and adjusting the internal read offset. This would be both more performant and cleaner:

[Symbol.iterator]() {
return this[Symbol.asyncIterator]();
}
*[Symbol.asyncIterator]() {
let frame: T | null;
do {
frame = this.read();
if (frame !== null) {
yield frame;
}
} while (frame !== null);
}
}

Here’s the process that runs every time .read() is called:

#read(sampleCount: number): T | null {
if (!sampleCount) {
return null;
}
const remainingSampleCount = this.#remainingSamples();
if (remainingSampleCount < sampleCount) {
return null;
}
let view = this.#view().subarray(
this.#readOffset,
this.#readOffset + sampleCount
) as T;
this.#readOffset += sampleCount;
if (this.#readOffset === this.#writeOffset) {
this.#writeOffset = 0;
this.#readOffset = 0;
}
if (this.#full()) {
const drained = this.drain();
if (drained) {
view = view.slice(0) as T;
this.write(drained);
}
}
return view;
}

While .read() is effective, it's not ideal when the caller wants more control over performance or data flow batching.


✅ Proposal

Add a method like:

readFrames(frameCount: number): T[] | null;

If not enough frames are available, return null. Otherwise, return the batch.

Example

const rb = new RingBufferF32({ frameCacheSize: 10 });

const batch = rb.readFrames(100);

class RingBufferBase {
  readFrames(frameCount: number): T[] | null {
    if (this.#availableFrames() < frameCount) return null;

    const batchArrayBuffer = new ArrayBuffer(
      frameCount * this.#TypedArrayConstructor.BYTES_PER_ELEMENT
    );
    // Fill and return typed view from internal buffer
  }
}

🧠 Implementation Suggestions

This feature must respect and integrate with the current frameCacheSize semantics.

🔁 Refactor .read() to use batch API

read(): T | null;
read(frameCount: number): T[] | null;

📦 Fixed-size batch buffer

If frameCacheSize >= 1, internally allocate a reusable memory block:

readonly #batch = new ArrayBuffer(frameSizeInBytes * frameCacheSize);

We could optionally introduce a:

discardBatchReadingBuffer?: boolean;

If true, the batch buffer is invalidated after read. This is ideal when the caller wants to transfer ownership to a WebWorker, AudioWorklet, etc. These use cases are exactly where ring buffers shine.


⚖️ Edge Cases and Behavior

Case: frameCacheSize === 0

Refer to #1 for context.

Option A

Disallow batch reading entirely. Always return null if frameCount > 1.

Option B

Use .subarray() to return a read-only reference to a slice. This respects the zero-copy semantics expected when frameCacheSize is 0.


📉 Limitations

  • The maximum batch size may be limited to frameCacheSize.
  • If discardBatchReadingBuffer is false, the returned memory must not be modified externally.
  • Batch reads should wait until frameCount frames are available—no partial results.

🧵 Final Note

It works today, but we can make it better.

This change would make ringbud friendlier for real-time data pipelines, audio streaming, and high-performance use cases. It's non-breaking and eligible for a minor release.

Would love feedback on:

  • Option A vs Option B for zero-copy scenarios
  • Default value for discardBatchReadingBuffer
  • Whether partial batch reads should be allowed

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions