🚀 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
🚀 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:ringbud/src/RingBufferBase.ts
Lines 281 to 294 in 55fef42
Here’s the process that runs every time
.read()is called:ringbud/src/RingBufferBase.ts
Lines 214 to 243 in 55fef42
✅ Proposal
Add a method like:
If not enough frames are available, return
null. Otherwise, return the batch.Example
🧠 Implementation Suggestions
This feature must respect and integrate with the current
frameCacheSizesemantics.🔁 Refactor
.read()to use batch API.read()a shortcut forreadFrames(1).📦 Fixed-size batch buffer
If
frameCacheSize >= 1, internally allocate a reusable memory block:We could optionally introduce a:
If
true, the batch buffer is invalidated after read. This is ideal when the caller wants to transfer ownership to aWebWorker,AudioWorklet, etc. These use cases are exactly where ring buffers shine.⚖️ Edge Cases and Behavior
Case:
frameCacheSize === 0Refer to #1 for context.
Option A
Disallow batch reading entirely. Always return
nullifframeCount > 1.Option B
Use
.subarray()to return a read-only reference to a slice. This respects the zero-copy semantics expected whenframeCacheSizeis0.📉 Limitations
frameCacheSize.discardBatchReadingBufferisfalse, the returned memory must not be modified externally.frameCountframes are available—no partial results.🧵 Final Note
It works today, but we can make it better.
This change would make
ringbudfriendlier 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:
discardBatchReadingBuffer