The Overrun Problem

Real-time audio processing on mobile devices operates under brutal constraints: you have roughly 5.8ms at 44.1kHz with a 256-sample buffer to read input, process it, and write output before the hardware demands the next block. Miss that deadline and you get silence, clicks, or worse—corrupted state that cascades into subsequent frames.

In production audio apps like hearing aids or voice processors, buffer overruns happen. A background notification triggers priority inversion. The thermal governor throttles your performance core mid-frame. A GC pause on Android steals 12ms. When Omar shipped HearingAid Pro, telemetry showed overruns occurring in 0.3% of frames under normal use, spiking to 2% during heavy multitasking. The question isn't whether overruns happen—it's how you recover without corrupting your DSP state.

Naive Circular Buffer Implementation

Most circular buffers for audio look like this:

class CircularBuffer {
  var buffer: [Float]
  var writeIndex: Int = 0
  var readIndex: Int = 0
  let capacity: Int
  
  func write(_ samples: [Float]) {
    for sample in samples {
      buffer[writeIndex % capacity] = sample
      writeIndex += 1
    }
  }
  
  func read(_ count: Int) -> [Float] {
    var result = [Float]()
    for _ in 0.. capacity || currentGap < 0
  }
}

Check hasOverrun at the start of every audio callback. If true, you're in recovery mode. The currentGap tells you how far behind you are—critical for choosing a recovery strategy.

Recovery: The Crossfade Approach

When you detect an overrun, the goal is to resynchronize read and write pointers without introducing artifacts. A hard jump creates a discontinuity that manifests as a click. The solution: crossfade between the corrupted region and the new valid data.

func recoverFromOverrun(gap: Int) {
  let safetyMargin = capacity / 4
  let newReadIndex = writeIndex - UInt64(safetyMargin)
  
  // Crossfade length: 32 samples at 44.1kHz = 0.7ms
  let fadeLength = 32
  var fadeBuffer = [Float](repeating: 0, count: fadeLength)
  
  for i in 0.. 100 {
      processingTimes.removeFirst()
    }
    
    if duration > warningThreshold {
      // Reduce quality, disable non-critical processing
      adaptProcessingLoad()
    }
  }
  
  func adaptProcessingLoad() {
    // Drop to 22.05kHz processing
    // Disable reverb tail
    // Simplify EQ from 10-band to 3-band
  }
}

When you consistently hit 80% of your deadline, preemptively reduce load. This is how HearingAid Pro maintained