Skip to content
Blog

Jitter and Buffering: Smoothing Audio Streams in Voice Agents

Learn how jitter buffers work in voice AI systems and how to optimize buffer settings for low-latency, high-quality conversational experiences.

Published on September 13, 2026

AI Assistant

Jitter and Buffering: Smoothing Audio Streams in Voice Agents

In real-time voice AI, audio packets don’t arrive at regular intervals. Network jitter creates timing variations that can destroy audio quality and break conversational flow. Jitter buffers solve this problem—but tuning them for AI agents requires a different approach than traditional VoIP.

The Jitter Problem

Why Packets Arrive Unevenly

Voice audio is transmitted in small packets (typically 20-60ms). Network conditions cause these packets to arrive with variable delays:

Sent:     [1] [2] [3] [4] [5] [6] [7] [8]  (even spacing)
Received: [1] [3] [2] [5] [4] [7] [6] [8]  (jittered)

Without buffering, this creates:

  • Audio gaps: Missing packets cause silence
  • Timing errors: Out-of-order playback distorts speech
  • ASR failures: Transcription models expect continuous audio

Jitter Buffer Basics

A jitter buffer holds packets briefly to reorder and smooth playback:

from collections import deque
import time

class SimpleJitterBuffer:
    def __init__(self, target_delay_ms: float = 60.0, 
                 max_delay_ms: float = 200.0):
        self.target_delay_ms = target_delay_ms
        self.max_delay_ms = max_delay_ms
        self.buffer = deque()
        self.last_playout_time = 0
        
    def add_packet(self, packet: dict):
        """Add packet to buffer with timestamp."""
        self.buffer.append({
            'data': packet['data'],
            'timestamp': packet['timestamp'],
            'arrival_time': time.time() * 1000
        })
        
        # Sort by timestamp to handle reordering
        self.buffer = deque(sorted(
            self.buffer, 
            key=lambda x: x['timestamp']
        ))
    
    def get_next_packet(self) -> dict:
        """Get next packet for playback when due."""
        if not self.buffer:
            return None
            
        current_time = time.time() * 1000
        packet = self.buffer[0]
        
        # Check if packet is due for playout
        playout_time = packet['arrival_time'] + self.target_delay_ms
        
        if current_time >= playout_time:
            self.buffer.popleft()
            return packet
        
        return None

Tuning Jitter Buffers for AI Agents

The AI Agent Difference

Traditional VoIP optimizes for human listening. AI voice agents have different requirements:

# Human-listening optimized (traditional)
human_config = {
    'target_delay_ms': 100,  # Higher delay OK for smoothness
    'max_delay_ms': 300,
    'min_delay_ms': 20
}

# AI-agent optimized
ai_config = {
    'target_delay_ms': 40,   # Lower delay for faster response
    'max_delay_ms': 80,      # Tighter bounds
    'min_delay_ms': 10       # Aggressive minimum
}

Adaptive Jitter Buffer

Implement adaptive buffering based on network conditions:

class AdaptiveJitterBuffer:
    def __init__(self):
        self.target_delay_ms = 60.0
        self.min_delay_ms = 20.0
        self.max_delay_ms = 150.0
        self.jitter_history = []
        self.history_size = 100
        
    def update_jitter_estimate(self, packet_interval_ms: float, 
                              actual_interval_ms: float):
        """Update jitter estimate based on packet timing."""
        jitter = abs(actual_interval_ms - packet_interval_ms)
        self.jitter_history.append(jitter)
        
        if len(self.jitter_history) > self.history_size:
            self.jitter_history.pop(0)
        
        # Calculate target delay based on 95th percentile jitter
        if len(self.jitter_history) >= 10:
            sorted_jitter = sorted(self.jitter_history)
            p95_index = int(len(sorted_jitter) * 0.95)
            p95_jitter = sorted_jitter[p95_index]
            
            # Set target delay to 1.5x P95 jitter
            self.target_delay_ms = max(
                self.min_delay_ms,
                min(self.max_delay_ms, p95_jitter * 1.5)
            )
    
    def get_target_delay(self) -> float:
        """Get current target delay."""
        return self.target_delay_ms

Packet Loss Concealment

Handling Missing Packets

When packets are lost, concealment techniques fill the gaps:

class PacketLossConcealment:
    def __init__(self, sample_rate: int = 16000):
        self.sample_rate = sample_rate
        self.last_packet = None
        
    def conceal_loss(self, missing_samples: int) -> np.ndarray:
        """Generate concealment audio for missing samples."""
        if self.last_packet is None:
            # Silence if no previous packet
            return np.zeros(missing_samples)
        
        # Simple repetition-based concealment
        # Repeat last packet's tail with fade-out
        concealment_length = min(missing_samples, len(self.last_packet))
        concealment = self.last_packet[-concealment_length:].copy()
        
        # Apply fade-out to avoid clicks
        fade_samples = min(100, concealment_length)
        fade_out = np.linspace(1.0, 0.0, fade_samples)
        concealment[-fade_samples:] *= fade_out
        
        # Pad with silence if needed
        if concealment_length < missing_samples:
            padding = np.zeros(missing_samples - concealment_length)
            concealment = np.concatenate([concealment, padding])
        
        return concealment
    
    def update_last_packet(self, packet: np.ndarray):
        """Update last received packet."""
        self.last_packet = packet

Forward Error Correction (FEC)

Use codec-level FEC for proactive loss recovery:

class OpusFECDecoder:
    """Opus codec with in-band FEC for packet loss recovery."""
    
    def __init__(self):
        # Opus FEC embeds previous frame in current packet
        self.previous_frame = None
        
    def decode_with_fec(self, packet: bytes, 
                       packet_lost: bool = False) -> np.ndarray:
        """Decode packet with FEC recovery."""
        if packet_lost and self.previous_frame is not None:
            # Use FEC data from previous packet
            return self.decode_fec_from_previous()
        
        # Normal decode
        decoded = self.decode_opus(packet)
        self.previous_frame = decoded
        return decoded
    
    def decode_fec_from_previous(self) -> np.ndarray:
        """Extract FEC data from previous packet."""
        # FEC data is embedded in the Opus packet
        # Implementation depends on Opus library
        pass

Optimizing for Barge-In

The Barge-In Challenge

When users interrupt, buffered audio must be flushed immediately:

class BargeInHandler:
    def __init__(self):
        self.outbound_buffer = []
        self.is_playing = False
        
    def handle_barge_in(self):
        """Flush buffered audio on user interruption."""
        # Clear outbound buffer
        self.outbound_buffer.clear()
        self.is_playing = False
        
        # Clear playback device buffer
        self.clear_playback_device()
        
        # Reset TTS stream if active
        self.reset_tts_stream()
    
    def clear_playback_device(self):
        """Clear audio device buffer."""
        # Platform-specific implementation
        pass

Transport Layer Flush

Clean interruption requires flushing across the media path:

class WebRTCBargeInManager:
    def __init__(self, peer_connection):
        self.pc = peer_connection
        self.outbound_track = None
        
    async def interrupt_agent_speech(self):
        """Flush all queued audio across WebRTC transport."""
        # Stop sending new audio
        if self.outbound_track:
            self.outbound_track.enabled = False
        
        # Flush SFU-side buffers
        await self.flush_sfu_buffers()
        
        # Flush client-side playout buffer
        await self.flush_client_buffer()
        
        # Re-enable for new speech
        if self.outbound_track:
            self.outbound_track.enabled = True
    
    async def flush_sfu_buffers(self):
        """Flush Selective Forwarding Unit buffers."""
        # SFU-specific implementation
        pass

Configuration by Network Type

Stable Broadband

broadband_config = {
    'target_delay_ms': 30,
    'max_delay_ms': 60,
    'min_delay_ms': 10,
    'plc_enabled': True,
    'fec_enabled': True
}

Mobile/LTE

mobile_config = {
    'target_delay_ms': 50,
    'max_delay_ms': 100,
    'min_delay_ms': 20,
    'plc_enabled': True,
    'fec_enabled': True,
    'adaptive_enabled': True
}

Lossy Network

lossy_config = {
    'target_delay_ms': 80,
    'max_delay_ms': 150,
    'min_delay_ms': 40,
    'plc_enabled': True,
    'fec_enabled': True,
    'retransmission_enabled': True
}

Monitoring and Metrics

Key Metrics to Track

class JitterBufferMetrics:
    def __init__(self):
        self.metrics = {
            'avg_jitter_ms': 0,
            'max_jitter_ms': 0,
            'packet_loss_rate': 0,
            'buffer_overflow_count': 0,
            'avg_buffer_level_ms': 0
        }
        
    def record_packet(self, jitter_ms: float, lost: bool):
        """Record packet statistics."""
        self.metrics['avg_jitter_ms'] = (
            self.metrics['avg_jitter_ms'] * 0.95 + jitter_ms * 0.05
        )
        self.metrics['max_jitter_ms'] = max(
            self.metrics['max_jitter_ms'], jitter_ms
        )
        
        if lost:
            self.metrics['packet_loss_rate'] = (
                self.metrics['packet_loss_rate'] * 0.99 + 0.01
            )

Alerting Thresholds

class JitterBufferAlerts:
    def __init__(self):
        self.thresholds = {
            'high_jitter_ms': 100,
            'high_loss_rate': 0.05,
            'buffer_overflow_rate': 0.01
        }
    
    def check_alerts(self, metrics: dict) -> list:
        """Check for concerning conditions."""
        alerts = []
        
        if metrics['avg_jitter_ms'] > self.thresholds['high_jitter_ms']:
            alerts.append('HIGH_JITTER')
        
        if metrics['packet_loss_rate'] > self.thresholds['high_loss_rate']:
            alerts.append('HIGH_PACKET_LOSS')
        
        return alerts

Best Practices

1. Measure Before Tuning

# Establish baseline
baseline = measure_jitter_buffer_performance(
    duration_seconds=300,
    sample_rate=100  # ms between samples
)

print(f"P50 jitter: {baseline['p50_jitter_ms']}ms")
print(f"P95 jitter: {baseline['p95_jitter_ms']}ms")
print(f"Packet loss: {baseline['packet_loss_rate']*100}%")

2. Use Aggressive Defaults for AI

# Start with AI-optimized settings
config = {
    'target_delay_ms': 40,
    'max_delay_ms': 80,
    'min_delay_ms': 15
}

# Adjust based on measured conditions
if measured_p95_jitter > 50:
    config['max_delay_ms'] = 120

3. Implement Graceful Degradation

class GracefulJitterBuffer:
    def __init__(self):
        self.normal_config = {'target_delay_ms': 40}
        self.degraded_config = {'target_delay_ms': 80}
        self.current_config = self.normal_config
        
    def handle_degradation(self, condition: str):
        """Switch to degraded mode on poor conditions."""
        if condition == 'high_jitter':
            self.current_config = self.degraded_config
        elif condition == 'packet_loss':
            self.current_config = self.degraded_config

4. Monitor End-to-End Latency

class EndToEndLatencyMonitor:
    def __init__(self):
        self.samples = []
        
    def measure_turn_latency(self, user_speech_end: float,
                            agent_speech_start: float):
        """Measure time from user stop to agent start."""
        latency_ms = (agent_speech_start - user_speech_end) * 1000
        self.samples.append(latency_ms)
        
        if len(self.samples) > 100:
            self.samples.pop(0)
        
        return latency_ms

Conclusion

Jitter buffers are essential for voice AI, but they require careful tuning:

  1. AI agents need lower latency than human listening
  2. Adaptive buffers respond to changing network conditions
  3. Packet loss concealment prevents audio gaps
  4. Barge-in requires flushing across the entire media path
  5. Monitor continuously to catch degradation early

The key insight: every millisecond of jitter buffer delay adds to the user’s perceived latency. For AI voice agents, where the total budget is often under 1000ms, buffer tuning can make the difference between a natural conversation and a frustrating experience.

Start with aggressive settings, measure real-world performance, and adjust based on your specific network conditions. The optimal configuration is always a balance between latency and audio quality.