Buffer Pool Architecture in Meetily: Reducing Allocation Overhead During Audio Processing

Meetily eliminates per-frame heap allocations during real-time audio processing by using a thread-safe AudioBufferPool that pre-allocates reusable Vec<f32> buffers and manages them through RAII-wrapped PooledBuffer objects.

Real-time audio processing in Zackriya-Solutions/meetily requires handling continuous streams of f32 samples without garbage collection pauses or allocation latency. To solve this, Meetily employs a buffer pool architecture that recycles memory buffers instead of allocating fresh vectors for every audio frame, ensuring predictable performance during capture, VAD, and transcription workflows.

Core Components of the Buffer Pool Architecture

AudioBufferPool

The AudioBufferPool struct defined in frontend/src-tauri/src/audio/buffer_pool.rs (lines 4-9) encapsulates a thread-safe VecDeque of pre-allocated Vec<f32> buffers. It enforces two configuration parameters: max_size limiting the number of retained buffers, and buffer_capacity specifying the sample capacity per buffer.

PooledBuffer RAII Wrapper

Defined at lines 73-78 in the same file, PooledBuffer acts as a smart pointer that automatically returns its buffer to the pool when dropped. This guarantees buffer reuse even during early returns or panics, preventing memory leaks without explicit return_buffer() calls.

RecordingState Integration

The global recording state owns a single pool instance created via AudioBufferPool::new(16, 48_000) (lines 39-40 in frontend/src-tauri/src/audio/recording_state.rs). This configuration provides up to 16 buffers, each capable of storing one second of 48 kHz audio data.

How the Buffer Pool Reduces Allocation Overhead

The architecture follows a four-phase lifecycle to minimize allocation overhead:

  1. Pool Creation: When RecordingState initializes, it constructs the pool with AudioBufferPool::new(max_size, buffer_capacity). The underlying VecDeque starts empty but reserves capacity for max_size buffers.

  2. Buffer Acquisition: Calling pool.get_buffer() either pops a ready-to-use buffer from the front of the deque or allocates a fresh Vec with Vec::with_capacity(buffer_capacity). The returned vector's length is reset to 0 while preserving its underlying capacity.

  3. Buffer Return: After processing, pool.return_buffer(buf) pushes the buffer to the back of the deque if the pool hasn't reached max_size. If the pool is full, the extra buffer is dropped, allowing the system allocator to reclaim the memory.

  4. RAII Convenience: PooledBuffer::new(pool.clone()) fetches a buffer and stores it inside a wrapper. When the wrapper goes out of scope, its Drop implementation automatically calls pool.return_buffer(buf), eliminating manual return calls and ensuring safety across all exit paths.

Thread Safety and Concurrency

The pool is wrapped in an Arc<Mutex<_>>, enabling safe sharing across threads handling audio capture, voice activity detection (VAD), mixing, and Whisper transcription. While the Mutex introduces a synchronization point, its cost is negligible compared to the overhead of repeated heap allocations for large audio vectors.

Memory and Performance Benefits

  • Reduced Heap Churn: Reusing memory blocks prevents repeated malloc/free cycles that fragment the heap and degrade real-time performance.
  • Predictable Latency: The pool guarantees buffer availability up to max_size; even when exhausted, fresh allocations use pre-determined capacities to avoid expensive reallocations.
  • Deterministic Footprint: The hard cap of 16 buffers limits memory usage to approximately 12 MiB (16 × 48,000 samples × 4 bytes), preventing unbounded growth during sustained audio processing.

Implementation Example

use frontend::src_tauri::src::audio::{AudioBufferPool, PooledBuffer};

// Create a pool that keeps 8 buffers, each large enough for 0.5s of 48kHz audio
let pool = AudioBufferPool::new(8, 48_000 / 2);

// Manual get/return pattern
let mut buf = pool.get_buffer();          // Borrow a Vec<f32>
buf.extend_from_slice(&[0.0, 0.1, 0.2]);   // Fill with samples
// ... audio processing ...
pool.return_buffer(buf);                  // Hand back for reuse

// RAII style with automatic return
{
    let mut pooled = PooledBuffer::new(pool.clone());
    // `pooled` derefs to `Vec<f32>`
    pooled.extend([0.3, 0.4, 0.5].iter().cloned());
    // Buffer automatically returned when `pooled` goes out of scope
}

Key Source Files

File Purpose
frontend/src-tauri/src/audio/buffer_pool.rs Implementation of AudioBufferPool and PooledBuffer.
frontend/src-tauri/src/audio/recording_state.rs Holds the global AudioBufferPool instance and shows its creation (AudioBufferPool::new(16, 48_000)).
frontend/src-tauri/src/audio/mod.rs Re-exports AudioBufferPool and PooledBuffer for easy import elsewhere.

Summary

  • Meetily's buffer pool architecture eliminates per-frame heap allocations by recycling Vec<f32> buffers through a thread-safe AudioBufferPool.
  • The PooledBuffer RAII wrapper ensures buffers are automatically returned to the pool when dropped, preventing leaks during early returns or panics.
  • Configured with 16 buffers at 48,000 samples each, the pool caps memory usage at approximately 12 MiB while serving real-time audio threads.
  • The implementation uses Arc<Mutex<_>> to safely share the pool across capture, VAD, mixing, and Whisper transcription pipelines.

Frequently Asked Questions

What is the buffer pool architecture in Meetily?

The buffer pool architecture is a memory management strategy that pre-allocates a fixed number of reusable Vec<f32> buffers to avoid repeated heap allocations during real-time audio processing. It consists of the AudioBufferPool struct that manages available buffers, the PooledBuffer RAII wrapper for automatic returns, and a global instance owned by RecordingState that threads share via Arc<Mutex<_>>.

How does PooledBuffer ensure buffers are returned to the pool?

PooledBuffer implements the Drop trait to automatically call pool.return_buffer(buf) when the wrapper goes out of scope. This pattern guarantees that buffers are recycled even if the processing function returns early or panics, eliminating manual memory management errors.

What happens when the pool is exhausted?

When get_buffer() is called on an empty pool, it allocates a fresh Vec<f32> using Vec::with_capacity(buffer_capacity). When return_buffer() is called on a full pool, the extra buffer is dropped instead of being stored. This prevents unbounded memory growth while ensuring the pool never exceeds its configured max_size.

Why is Arc<Mutex<>> used for the buffer pool?

The Arc<Mutex<_>> wrapper allows thread-safe sharing of the pool across multiple audio processing stages (capture, VAD, mixing, transcription) that run concurrently. While the mutex adds a small synchronization cost, it is significantly cheaper than the heap allocation overhead it eliminates, and it ensures exclusive access to the VecDeque during push and pop operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →