PCM Linear, Codec Opus e Taxas de Amostragem para Voice AI — aula do módulo Codecs de Áudio, PCM, Opus e Buffering de Streaming, na trilha Fundamentos de…
Leitura de aproximadamente 8 minutos.
Módulo: Codecs de Áudio, PCM, Opus e Buffering de Streaming · Curso: Fundamentos de Áudio & Arquitetura Realtime · Formação: Formação em Voice AI & Agentes de Voz em Tempo Real
No desenvolvimento de agentes conversacionais de voz em tempo real (Voice AI), o áudio não é tratado como um arquivo estático transferido em lote, mas como um fluxo contínuo de dados brutos que deve ser amostrado, quantizado, empacotado e transportado sob restrições severas de latência (< 300 ms end-to-end). Compreender a física e a engenharia de sinais digitais — em especial o formato PCM (Pulse Code Modulation) e o codec Opus — é o primeiro passo para construir pipelines de voz resilientes e de alta fidelidade.
O som no mundo físico é uma onda mecânica contínua de pressão no ar. Para que um microfone envie esse sinal para uma LLM ou modelo de reconhecimento de fala (STT - Speech-to-Text), o sinal analógico passa por dois processos fundamentais executados pelo ADC (Analog-to-Digital Converter):
1. Amostragem (Sampling): Medição da amplitude da onda em intervalos regulares de tempo (taxa $f_s$ em Hertz). 2. Quantização (Quantization): Arredondamento do valor de amplitude contínuo para um nível discreto finito representado por $N$ bits (ex: 16 bits = 65.536 níveis possíveis).
┌───────────────────────────────────────────────────────────────────────────────────┐
│ DIGITALIZAÇÃO DE SINAL ACÚSTICO: AMOSTRAGEM E QUANTIZAÇÃO │
├───────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Sinal Analógico Contínuo (Voz Humana captada pelo Microfone): │
│ ~~\ /~~~\ │
│ \ / \ /~~~ │
│ \__/ \____/ │
│ │ │
│ ▼ [ADC: Amostragem Discreta a cada Ts = 1/fs (Taxa de Amostragem)] │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ s1│ │ s2│ │ s3│ │ s4│ │ s5│ │ s6│ Amplitudes Discretas no Tempo │
│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │
│ │ │
│ ▼ [Quantização 16-bit Linear PCM (Signed Int16: -32768 a +32767)] │
│ Array de Bytes: [0x1A, 0x04, 0xFE, 0x12, ...] (Fluxo Little-Endian Bruto) │
│ │
└───────────────────────────────────────────────────────────────────────────────────┘
Pelo Teorema de Nyquist-Shannon, para reconstruir com precisão um sinal sem sofrer aliasing (distorção por frequências espúrias), a taxa de amostragem deve ser de pelo menos o dobro da frequência máxima audível desejada ($f_s \ge 2 \cdot f_{max}$).
A voz humana possui energia acústica concentrada entre 300 Hz e 3.400 Hz (banda de telefonia tradicional), mas formantes de clareza, fricativas ("s", "f", "ch") e riqueza tímbrica se estendem até 8 kHz ou 12 kHz.
| Taxa ($f_s$) | Freq. Nyquist | Largura de Banda PCM (16-bit Mono) | Aplicação Típica em IA | Exemplo de Modelos | |---|---|---|---|---| | 8.000 Hz | 4.000 Hz | 128 kbps (16 KB/s) | Telefonia PSTN / G.711 legado | Twilio Phone Call (μ-law/a-law) | | 16.000 Hz | 8.000 Hz | 256 kbps (32 KB/s) | Padrão Ouro de Modelos STT / VAD | Whisper, Deepgram Nova-2, Silero VAD v5/v6 | | 24.000 Hz | 12.000 Hz | 384 kbps (48 KB/s) | Padrão Ouro de Modelos Multimodais / TTS | OpenAI Realtime API (GPT-Realtime-2.1), Gemini 3.8 Live | | 48.000 Hz | 24.000 Hz | 768 kbps (96 KB/s) | Áudio Hi-Fi de Estúdio / Codec Opus nativo | WebRTC Audio Tracks, Discord, LiveKit |
[!IMPORTANT]
Por que 16 kHz e 24 kHz dominam a IA de Voz?
Quase todos os modelos de STT (como Whisper e Conformer) foram treinados em áudio a 16 kHz mono. Enviar áudio a 48 kHz para um modelo de STT apenas desperdiça largura de banda de rede e ciclos de CPU com downsampling. Por outro lado, modelos de TTS modernos (ElevenLabs, Cartesia, GPT-Realtime-2.1) operam nativamente a 24 kHz para entregar vozes naturais e calorosas sem o peso excessivo de 48 kHz.
O PCM linear é a representação pura, sem compressão, onde cada amostra é um número inteiro de 16 bits com sinal (signed 16-bit LE). É a "linguagem nativa" dos buffers de memória de áudio e das bibliotecas de processamento matemático (NumPy, PyTorch).
Opus é o padrão aberto e irrestrito da IETF para compressão de voz e áudio em tempo real. Ele combina a tecnologia SILK (otimizada para voz humana em baixos bitrates via predição linear) com a tecnologia CELT (otimizada para música e baixíssima latência baseada em MDCT). Versões recentes da libopus (1.6+) introduzem extensão neural de banda (BWE) e suporte avançado a Deep Redundancy (DRED).
┌───────────────────────────────────────────────────────────────────────────────────┐
│ ARQUITETURA HÍBRIDA DO CODEC OPUS (RFC 6716 / LIBOPUS 1.6) │
├───────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐ │
│ │ Codec Opus Multimodal │ │
│ │ ┌─────────────┐ ┌─────────────┐ │ │
│ Voz Humana (Baixo BR) │ │ SILK Engine │ │ CELT Engine │ │ Alta Fidelidade/Música │
│ ─────────────────────►│ │ (Modelagem │ │ (MDCT Ultra │ │◄────────────────────── │
│ │ │ Linear LP) │ │ Low Latency)│ │ │
│ │ └──────┬──────┘ └──────┬──────┘ │ │
│ │ └───────┬───────┘ │ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ Pacotes Opus Compactados (6 kbps a 128 kbps) │
│ Tamanhos de Frame Padronizados: 10ms, 20ms, 40ms │
│ Proteção contra Perdas: DRED + PLC (Packet Loss Concealment) │
│ │
└───────────────────────────────────────────────────────────────────────────────────┘
Abaixo apresentamos uma classe utilitária de produção para calcular métricas de buffers PCM, validar tamanhos de frames e extrair energia RMS (Root Mean Square) para monitoramento de sinal.
"""
Módulo de Utilidades PCM e Validação de Streaming de Áudio
Autor: Juliano Ceconi Academy - Voice AI Realtime
"""
import math
import struct
from typing import Generator, List
class PCM16StreamHelper:
def __init__(self, sample_rate: int = 16000, channels: int = 1):
"""
Inicializa o manipulador de chunks PCM 16-bit Linear.
:param sample_rate: Taxa de amostragem em Hz (ex: 16000, 24000)
:param channels: 1 para Mono, 2 para Stereo
"""
self.sample_rate = sample_rate
self.channels = channels
self.bytes_per_sample = 2 # 16 bits = 2 bytes
self.bytes_per_second = self.sample_rate * self.channels * self.bytes_per_sample
def calculate_frame_bytes(self, duration_ms: int) -> int:
"""Calcula a quantidade exata de bytes para uma dada duração em milissegundos."""
num_samples = int((self.sample_rate * duration_ms) / 1000)
return num_samples * self.channels * self.bytes_per_sample
def split_into_frames(
self, raw_pcm_bytes: bytes, frame_duration_ms: int = 20
) -> Generator[bytes, None, None]:
"""
Divide um stream contínuo de bytes PCM em frames atômicos de duração fixa (ex: 20ms).
Essencial para envio em WebSockets e buffers de VAD.
"""
frame_size = self.calculate_frame_bytes(frame_duration_ms)
for i in range(0, len(raw_pcm_bytes), frame_size):
chunk = raw_pcm_bytes[i : i + frame_size]
if len(chunk) == frame_size:
yield chunk
def calculate_rms_dbfs(self, pcm_chunk: bytes) -> float:
"""
Calcula o volume em dBFS (Decibels relative to Full Scale) de um frame PCM 16-bit.
Retorna valores de -96.0 dBFS (silêncio total) a 0.0 dBFS (pico máximo de saturação).
"""
num_samples = len(pcm_chunk) // self.bytes_per_sample
if num_samples == 0:
return -96.0
# Unpack de inteiros com sinal 16-bit little-endian ('<h')
format_str = f"<{num_samples}h"
samples: tuple = struct.unpack(format_str, pcm_chunk)
sum_squares = sum(s * s for s in samples)
mean_square = sum_squares / num_samples
rms = math.sqrt(mean_square)
if rms <= 0:
return -96.0
# 32767.0 é o valor máximo positivo em int16
dbfs = 20 * math.log10(rms / 32767.0)
return max(dbfs, -96.0)
# Demonstração de uso
if __name__ == "__main__":
helper = PCM16StreamHelper(sample_rate=24000, channels=1)
frame_20ms_bytes = helper.calculate_frame_bytes(20)
print(f"[Info] Tamanho de frame para 20ms a 24kHz (PCM16): {frame_20ms_bytes} bytes (960 amostras)")
# Simula 100ms de áudio em silêncio (zeros) e um frame com sinal
fake_audio = b"\x00" * helper.calculate_frame_bytes(100)
for idx, frame in enumerate(helper.split_into_frames(fake_audio, 20)):
rms = helper.calculate_rms_dbfs(frame)
print(f" Frame {idx}: {len(frame)} bytes | Volume: {rms:.2f} dBFS")
[!WARNING]
A Armadilha do Formato do Browser (Float32) vs Modelos (Int16)Outras aulas do módulo