~/wiki

Rolling Buffers

Confiance : high
rolling-bufferscircular-buffersstreaming-dataaudio-processingreal-time-systemsmemory-managementcontinuous-inferencewake-word-detectionbuffer-managementstreaming-inferencemel-spectrogramembedding-modelstensor-processingfixed-size-windowsaudio-chunks

Data structures that maintain a fixed-size sliding window of elements, automatically discarding old data as new data arrives. Essential for streaming applications where you need to maintain context over time while controlling memory usage.

Core Concept

Sliding Window: Fixed-size buffer that "rolls" forward as new data arrives, keeping only the most recent N elements.

class RollingBuffer<T> {
    private var buffer: [T] = []
    private let maxSize: Int
    
    init(maxSize: Int) {
        self.maxSize = maxSize
    }
    
    func append(_ element: T) {
        buffer.append(element)
        if buffer.count > maxSize {
            buffer.removeFirst()
        }
    }
    
    func suffix(_ count: Int) -> ArraySlice<T> {
        return buffer.suffix(count)
    }
}

Audio Processing Applications

Wake-Word Detection Pipeline

Rolling buffers enable continuous audio processing by maintaining multiple overlapping context windows:

// Raw audio buffer for mel-spectrogram computation
rawAudioBuffer.append(newChunk)           // Add 80ms chunk
melInput = rawAudioBuffer.suffix(1760)    // Use last ~110ms for context

// Mel-frame buffer for embedding model
melFrameBuffer.append(newMelFrames)       // Add new frames  
embeddingInput = melFrameBuffer.suffix(76) // 76-frame window

// Embedding buffer for classification
embeddingBuffer.append(newEmbedding)      // Add 96-dim vector
classifierInput = embeddingBuffer.suffix(16) // Last 16 embeddings

Cascading Buffer Chain

Each stage maintains its own rolling buffer with appropriate window sizes:

  1. Audio Samples: 1760 samples (~110ms) for mel-spectrogram context
  2. Mel Frames: 76 frames for embedding model input window
  3. Embeddings: 16 vectors (~1.28s) for wake-word classification

Memory Management Benefits

Bounded Memory Usage

// Without rolling buffers - memory grows unbounded
var allAudioSamples: [Float] = []  // Grows forever
allAudioSamples.append(contentsOf: newChunk)

// With rolling buffers - fixed memory footprint
let audioBuffer = RollingBuffer<Float>(maxSize: 1760)
audioBuffer.append(contentsOf: newChunk)  // Auto-discards old data

Predictable Resource Usage

  • Audio Buffer: 1760 × 4 bytes = ~7KB
  • Mel Buffer: 76 × 32 × 4 bytes = ~10KB
  • Embedding Buffer: 16 × 96 × 4 bytes = ~6KB
  • Total: <25KB for complete pipeline context

Implementation Patterns

Startup Padding

Handle insufficient data during initialization:

func processAudioChunk(_ chunk: [Float]) -> Float? {
    rawAudioBuffer.append(contentsOf: chunk)
    
    // Need minimum samples for mel-spectrogram
    guard rawAudioBuffer.count >= minSamplesRequired else {
        return nil  // Skip inference until buffer fills
    }
    
    let melInput = rawAudioBuffer.suffix(1760)
    // Proceed with inference...
}

Buffer Synchronization

Coordinate multiple rolling buffers for pipeline consistency:

class StreamingPipeline {
    private let audioBuffer = RollingBuffer<Float>(maxSize: 1760)
    private let melBuffer = RollingBuffer<[Float]>(maxSize: 76)  
    private let embeddingBuffer = RollingBuffer<[Float]>(maxSize: 16)
    
    func process(_ chunk: [Float]) {
        // Stage 1: Audio → Mel
        audioBuffer.append(contentsOf: chunk)
        let newMelFrames = computeMelSpectrogram(audioBuffer.suffix(1760))
        
        // Stage 2: Mel → Embedding  
        melBuffer.append(contentsOf: newMelFrames)
        let newEmbedding = computeEmbedding(melBuffer.suffix(76))
        
        // Stage 3: Embedding → Classification
        embeddingBuffer.append(newEmbedding)
        if embeddingBuffer.count >= 16 {
            let confidence = classify(embeddingBuffer.suffix(16))
            return confidence
        }
    }
}

Performance Characteristics

Time Complexity

  • Append: O(1) amortized (Array.append + conditional removeFirst)
  • Suffix Access: O(k) where k is suffix length
  • Space: O(maxSize) fixed memory footprint

Optimization Strategies

// Circular buffer for O(1) operations
class CircularRollingBuffer<T> {
    private var buffer: [T?]
    private var head: Int = 0
    private var count: Int = 0
    private let capacity: Int
    
    func append(_ element: T) {
        buffer[head] = element
        head = (head + 1) % capacity
        count = min(count + 1, capacity)
    }
    
    func recentElements(_ k: Int) -> [T] {
        // Extract last k elements in order
        let start = (head - min(k, count) + capacity) % capacity
        // Implementation details...
    }
}

Real-Time Streaming Patterns

Continuous Processing Loop

func startStreaming() {
    audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { 
        [weak self] buffer, _ in
        
        let samples = Array(buffer.floatChannelData![0][0..<Int(buffer.frameLength)])
        
        self?.workerQueue.async {
            if let confidence = self?.pipeline.processChunk(samples) {
                if confidence > threshold {
                    DispatchQueue.main.async {
                        self?.onWakeWordDetected()
                    }
                }
            }
        }
    }
}

Buffer Warm-Up Strategy

// Pre-fill buffers with zeros to avoid startup delays
func initializeBuffers() {
    // Fill audio buffer with silence
    let silence = [Float](repeating: 0.0, count: 1760)
    audioBuffer.append(contentsOf: silence)
    
    // Pre-compute initial mel frames
    let initialMel = computeMelSpectrogram(silence)
    melBuffer.append(contentsOf: initialMel)
    
    // Note: First ~1.5s of detection may be noisier
}

Error Handling and Edge Cases

Buffer Underflow

func safeSuffix<T>(_ buffer: [T], _ count: Int) -> ArraySlice<T> {
    let availableCount = min(count, buffer.count)
    return buffer.suffix(availableCount)
}

Thread Safety

class ThreadSafeRollingBuffer<T> {
    private let queue = DispatchQueue(label: "rolling-buffer")
    private var buffer: [T] = []
    
    func append(_ element: T) {
        queue.sync {
            buffer.append(element)
            if buffer.count > maxSize {
                buffer.removeFirst()
            }
        }
    }
    
    func recentElements(_ count: Int) -> [T] {
        return queue.sync {
            return Array(buffer.suffix(count))
        }
    }
}

Use Cases Beyond Audio

Time Series Analytics

  • Metric Monitoring: Rolling window for moving averages
  • Anomaly Detection: Recent data points for trend analysis
  • Real-Time Dashboards: Latest N data points for visualization

Network Streaming

  • Video Buffering: Fixed-size frame buffer for smooth playback
  • Chat Systems: Recent message history with memory bounds
  • Game State: Rolling history for replay and prediction

Machine Learning

  • Online Learning: Recent samples for model updates
  • Feature Engineering: Temporal windows for sequence models
  • Real-Time Inference: Context maintenance for streaming predictions

See also