Objetivo y caso de uso
Qué construirás: Un intercomunicador dúplex completo con supresión de ruido utilizando Raspberry Pi Zero 2 W, un micrófono I2S y un amplificador I2S.
Para qué sirve
- Comunicación bidireccional en entornos ruidosos, como fábricas o talleres.
- Proyectos de domótica para intercomunicación entre habitaciones.
- Aplicaciones de asistencia para personas con discapacidad auditiva.
- Desarrollo de sistemas de alerta en vehículos o maquinaria pesada.
Resultado esperado
- Latencia de audio inferior a 50 ms en la transmisión de voz.
- Reducción de ruido de fondo en un 80% gracias a la implementación de RNNoise.
- Capacidad de manejar hasta 10 paquetes de audio por segundo sin pérdida de calidad.
- Consumo de energía inferior a 1 W durante la operación continua.
Público objetivo: Desarrolladores y entusiastas de la electrónica; Nivel: Avanzado
Arquitectura/flujo: Raspberry Pi Zero 2 W -> Micrófono I2S -> Procesamiento de audio -> Amplificador I2S -> Salida de audio.
Nivel: Avanzado
Prerrequisitos
- Sistema operativo:
- Raspberry Pi OS Bookworm 64‑bit (Debian 12), imagen “Lite” o “Full”.
- Kernel Linux 6.6.x (serie LTS en Bookworm para Raspberry Pi).
- Hardware exacto:
- Raspberry Pi Zero 2 W
- Adafruit SPH0645 I2S Mic
- MAX98357A I2S Class‑D Amplifier (Adafruit u otro breakout equivalente; se usa como DAC+amplificador I2S)
- Toolchain y versiones concretas para este caso:
- Python 3.11.2 (preinstalado en Raspberry Pi OS Bookworm de 64 bits)
- pip 24.2 (se actualizará explícitamente)
- gcc (Debian 12.2.0-14) 12.2.0
- g++ (Debian 12.2.0-14) 12.2.0
- CMake 3.25.1
- PortAudio 19.6.0 (vía paquete portaudio19-dev)
- ALSA lib 1.2.8
- Paquetes Python (se fijan versiones exactas):
- numpy==1.26.4
- sounddevice==0.4.6
- rnnoise==0.4.1
- pyyaml==6.0.1
- gpiozero==1.6.2 (opcional para PTT por GPIO)
- Herramientas/paquetes del sistema:
- build-essential, python3.11-venv, python3-dev, libasound2-dev, portaudio19-dev, librnnoise0, librnnoise-dev, alsa-utils, git
Notas:
– Raspberry Pi OS Bookworm utiliza por defecto PipeWire/WirePlumber en la edición “Full”. Para audio de baja latencia y control directo de I2S usaremos ALSA y dispositivos hw:…, evitando capas extra. Las instrucciones de este caso práctico abren los dispositivos ALSA en modo “hw” para reducir la latencia y maximizar la precisión.
Materiales
- Kit base:
- Raspberry Pi Zero 2 W
- Tarjeta microSD (≥16 GB, clase A1/A2 recomendada)
- Alimentación 5 V/2.5 A con cable micro‑USB de buena calidad
- Cabecera GPIO de 40 pines soldada en la Pi Zero 2 W (si no viene pre‑soldada)
- Audio I2S:
- Adafruit SPH0645 I2S Microphone (micrófono I2S, 3.3 V)
- MAX98357A I2S Class-D Amplifier (alimentación 5 V recomendada)
- Altavoz 4–8 Ω (3–5 W)
- Conexión:
- Cables dupont macho‑hembra x10–12
- Protoboard (opcional pero recomendable)
- Opcionales (para control de PTT local):
- Pulsador + resistencia 10 kΩ (pull‑down si no usamos internal pull‑up)
- LED + resistencia 330 Ω
Este caso práctico se centra exclusivamente en el modelo “Raspberry Pi Zero 2 W + Adafruit SPH0645 I2S Mic + MAX98357A Amp”. Todo el cableado, configuración, código y validación asume este conjunto.
Preparación y conexión
Habilitar I2S y configurar la tarjeta combinada
Usaremos el overlay del kernel “googlevoicehat-soundcard”, que configura simultáneamente:
– Entrada por micrófono I2S (compatible con SPH0645)
– Salida por DAC/amp I2S (compatible con MAX98357A)
Este overlay configura la interfaz I2S de la Pi (BCLK, LRCLK, DIN, DOUT) en los pines estándar y crea un único dispositivo ALSA full‑duplex, ideal para un intercomunicador con micrófono y altavoz I2S.
Pasos:
1) Edita el fichero de arranque (como root):
sudo nano /boot/firmware/config.txt
2) Añade al final (evitando duplicados):
dtparam=audio=off
dtoverlay=googlevoicehat-soundcard
3) Guarda y reinicia:
sudo reboot
4) Tras reiniciar, verifica los dispositivos ALSA:
arecord -l
aplay -l
Deberías ver una tarjeta similar a “snd-googlevoicehat” o con descripción “Google voiceHAT SoundCard”. Tomaremos esta tarjeta como hw:0,0 en el resto de pasos.
Conexión de pines
La interfaz I2S estándar de Raspberry Pi usa los siguientes pines GPIO:
- BCLK: GPIO18 (PCM_CLK)
- LRCLK: GPIO19 (PCM_FS)
- DIN (entrada a la Pi): GPIO20 (PCM_DIN)
- DOUT (salida desde la Pi): GPIO21 (PCM_DOUT)
El micrófono I2S (SPH0645) entrega datos al pin DIN de la Pi (PCM_DIN). El MAX98357A recibe datos desde el pin DOUT de la Pi (PCM_DOUT). Ambos comparten BCLK y LRCLK.
Tabla de cableado recomendado:
| Señal/Alimentación | Raspberry Pi Zero 2 W (GPIO) | Adafruit SPH0645 I2S Mic | MAX98357A Amp |
|---|---|---|---|
| 3V3 | Pin 1 (3V3) | 3V (VIN) | No conectar (usar 5V) |
| 5V | Pin 2 o 4 (5V) | No conectar (3.3V solamente) | VIN (5V) |
| GND | Pin 6, 9, 14, 20, 25, 30, 34, 39 | GND | GND |
| I2S BCLK | GPIO18 (Pin 12) | BCLK | BCLK |
| I2S LRCLK | GPIO19 (Pin 35) | L/RCLK | LRC |
| I2S Data hacia Pi | GPIO20 (Pin 38) | DOUT | — |
| I2S Data desde Pi | GPIO21 (Pin 40) | — | DIN |
| SEL (canal del mic) | — | SEL a GND = canal izquierdo (recomendado) | — |
| GAIN / SD MODE | — | — | Config. por pines del módulo (opcional) |
Observaciones:
- Alimenta el SPH0645 estrictamente a 3.3 V. No uses 5 V en el micrófono.
- Alimenta el MAX98357A preferentemente con 5 V; de este modo obtienes potencia de salida adecuada. Conecta un altavoz de 4–8 Ω a las bornas del MAX98357A.
- Conecta BCLK y LRCLK en paralelo al mic y al amp desde la Pi.
- El SPH0645 tiene un pin SEL; a GND entrega datos por canal izquierdo; a 3V3 por canal derecho. Usaremos SEL a GND.
- Mantén cortos los cables I2S y de buena calidad para minimizar jitter y EMI.
Comprobación eléctrica básica
- Mide 3.3 V y 5 V con multímetro antes de alimentar definitivamente.
- Verifica continuidad y que no hay cortos entre 3V3 y GND.
- Enciende la Pi y confirma que el MAX98357A no se calienta en vacío y que el micrófono no muestra síntomas de alimentación incorrecta.
Código completo
A continuación implementaremos un intercomunicador full‑duplex por UDP entre dos nodos, con:
– Captura desde I2S (SPH0645) vía ALSA a 48 kHz mono
– Supresión de ruido en tiempo real con RNNoise
– Reproducción a I2S (MAX98357A) vía ALSA a 48 kHz mono
– Modo semi‑dúplex con PTT (push‑to‑talk) opcional por GPIO, para evitar realimentación acústica local
– Mecanismo VOX simple (activación por voz) opcional si no hay PTT
– Buffering de baja latencia (frames de 10 ms = 480 muestras a 48 kHz)
– Enlace UDP con control de jitter básico
Estructura:
– Una hebra captura->denoise->envía
– Otra hebra recibe->reproduce
– Sin bloqueo entre hebras usando colas y sockets no bloqueantes
Requisitos Python (en venv):
– numpy==1.26.4
– sounddevice==0.4.6
– rnnoise==0.4.1
– gpiozero==1.6.2 (opcional)
Archivo: intercom.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import argparse
import socket
import struct
import threading
import queue
import time
import sys
import signal
import numpy as np
import sounddevice as sd
from rnnoise import RNNoise
try:
from gpiozero import Button, LED
GPIO_AVAILABLE = True
except Exception:
GPIO_AVAILABLE = False
Button = None
LED = None
# Parámetros de audio
SAMPLE_RATE = 48000 # Hz
CHANNELS = 1 # Mono (mic I2S suele ser mono)
FRAME_MS = 10 # 10 ms
FRAME_SAMPLES = int(SAMPLE_RATE * FRAME_MS / 1000) # 480
PCM_FORMAT = 'int16' # Usaremos S16_LE
# Empaquetado de tramas UDP: cabecera simple (seq, ts)
HEADER_FORMAT = "!II" # seq (u32), timestamp_ms (u32)
class Intercom:
def __init__(self,
playback_device=None,
capture_device=None,
rx_port=6000,
tx_ip="192.168.1.100",
tx_port=6000,
ptt_gpio=None,
led_gpio=None,
vox_threshold=0.01,
vox_hold_ms=300,
denoise=True):
self.playback_device = playback_device
self.capture_device = capture_device
self.rx_port = rx_port
self.tx_ip = tx_ip
self.tx_port = tx_port
self.vox_threshold = vox_threshold
self.vox_hold_ms = vox_hold_ms
self.denoise_enabled = denoise
# Sockets
self.sock_rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock_rx.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock_rx.bind(("0.0.0.0", self.rx_port))
self.sock_rx.setblocking(False)
self.sock_tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock_tx.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# RNNoise
self.rnnoise = RNNoise() if self.denoise_enabled else None
# GPIO PTT y LED (opcional)
self.ptt_button = None
self.tx_led = None
self.ptt_active = False
if ptt_gpio is not None and GPIO_AVAILABLE:
self.ptt_button = Button(ptt_gpio, pull_up=True)
self.ptt_button.when_pressed = self._ptt_down
self.ptt_button.when_released = self._ptt_up
if led_gpio is not None and GPIO_AVAILABLE:
self.tx_led = LED(led_gpio)
# Control de ejecución
self.running = True
self.seq = 0
self.last_vox_ms = 0
# Buffers y streams
self.play_q = queue.Queue(maxsize=50) # Cola de reproducción
self.tx_lock = threading.Lock()
# Streams PortAudio/ALSA
self.in_stream = None
self.out_stream = None
def _ptt_down(self):
self.ptt_active = True
if self.tx_led:
self.tx_led.on()
def _ptt_up(self):
self.ptt_active = False
if self.tx_led:
self.tx_led.off()
def _should_transmit(self, frame):
# Si PTT existe, gobierna
if self.ptt_button is not None:
return self.ptt_active
# VOX simple si no hay PTT: energía RMS > umbral
rms = np.sqrt(np.mean((frame.astype(np.float32) / 32768.0) ** 2))
now_ms = int(time.time() * 1000)
if rms > self.vox_threshold:
self.last_vox_ms = now_ms
return True
# Mantener transmisión por hold_ms para no cortar sílabas
if now_ms - self.last_vox_ms < self.vox_hold_ms:
return True
return False
def _denoise(self, frame):
if not self.rnnoise:
return frame
# RNNoise espera 480 muestras a 48k por trama, int16
# Devuelve float32 [-1, 1]; convertimos de vuelta a int16
den = self.rnnoise.process_frame(frame)
den = np.clip(den, -1.0, 1.0)
return (den * 32767.0).astype(np.int16)
def audio_in_cb(self, indata, frames, time_info, status):
if status:
# Reporte de underrun/overrun de PortAudio
print(f"[IN] Status: {status}", file=sys.stderr)
# Convertir a int16
data = np.frombuffer(indata, dtype=np.int16)
# Denoise por tramas de 480
# sounddevice puede entregar 'frames' múltiplos de FRAME_SAMPLES
outbuf = []
for off in range(0, len(data), FRAME_SAMPLES):
chunk = data[off:off + FRAME_SAMPLES]
if len(chunk) < FRAME_SAMPLES:
break
if self.denoise_enabled:
chunk = self._denoise(chunk)
outbuf.append(chunk)
# Decidir transmisión (PTT/VOX)
if self._should_transmit(chunk):
# Construir paquete UDP
with self.tx_lock:
header = struct.pack(HEADER_FORMAT, self.seq, int(time.time() * 1000) & 0xFFFFFFFF)
self.seq = (self.seq + 1) & 0xFFFFFFFF
pkt = header + chunk.tobytes()
try:
self.sock_tx.sendto(pkt, (self.tx_ip, self.tx_port))
except Exception as e:
print(f"[TX] Error enviando: {e}", file=sys.stderr)
# Semidúplex: cuando transmitimos, silenciamos altavoz local para evitar acople
if self.tx_led:
# LED indica TX activo
if self._should_transmit(data[:FRAME_SAMPLES]):
self.tx_led.on()
else:
self.tx_led.off()
def audio_out_worker(self):
# Hilo que drena paquetes recibidos y los escribe en el stream de salida
while self.running:
# Recepción no bloqueante
try:
pkt, addr = self.sock_rx.recvfrom(1500)
if len(pkt) >= struct.calcsize(HEADER_FORMAT) + FRAME_SAMPLES * 2:
# Extraer cabecera
_seq, _ts = struct.unpack(HEADER_FORMAT, pkt[:8])
payload = pkt[8:]
# Rechazar si estamos en TX (semidúplex)
if self.ptt_button is not None and self.ptt_active:
continue
try:
self.out_stream.write(payload)
except sd.PortAudioError as e:
print(f"[OUT] PortAudioError: {e}", file=sys.stderr)
except BlockingIOError:
pass
except Exception as e:
print(f"[RX] Error: {e}", file=sys.stderr)
time.sleep(0.001)
def start(self):
# Configurar streams ALSA vía sounddevice/PortAudio
# Selección explícita de dispositivo (índice o nombre), si se proporcionó
in_dev = self.capture_device if self.capture_device is not None else None
out_dev = self.playback_device if self.playback_device is not None else None
self.in_stream = sd.RawInputStream(
samplerate=SAMPLE_RATE,
channels=CHANNELS,
dtype='int16',
blocksize=FRAME_SAMPLES,
device=in_dev,
callback=self.audio_in_cb,
latency='low'
)
self.out_stream = sd.RawOutputStream(
samplerate=SAMPLE_RATE,
channels=CHANNELS,
dtype='int16',
blocksize=FRAME_SAMPLES,
device=out_dev,
latency='low'
)
self.out_stream.start()
self.in_stream.start()
t = threading.Thread(target=self.audio_out_worker, daemon=True)
t.start()
def stop(self):
self.running = False
time.sleep(0.05)
try:
if self.in_stream:
self.in_stream.stop()
self.in_stream.close()
except:
pass
try:
if self.out_stream:
self.out_stream.stop()
self.out_stream.close()
except:
pass
if self.tx_led:
self.tx_led.off()
def main():
parser = argparse.ArgumentParser(description="i2s-noise-suppression-intercom")
parser.add_argument("--tx-ip", type=str, required=True, help="IP remota a la que enviar audio")
parser.add_argument("--tx-port", type=int, default=6000, help="Puerto UDP remoto")
parser.add_argument("--rx-port", type=int, default=6000, help="Puerto UDP local de escucha")
parser.add_argument("--capture-device", type=str, default=None, help="Dispositivo de captura (índice o nombre)")
parser.add_argument("--playback-device", type=str, default=None, help="Dispositivo de reproducción (índice o nombre)")
parser.add_argument("--ptt-gpio", type=int, default=None, help="GPIO BCM para PTT (opcional)")
parser.add_argument("--led-gpio", type=int, default=None, help="GPIO BCM para LED TX (opcional)")
parser.add_argument("--vox-threshold", type=float, default=0.01, help="Umbral VOX RMS (0..1)")
parser.add_argument("--vox-hold-ms", type=int, default=300, help="Tiempo de retención VOX (ms)")
parser.add_argument("--no-denoise", action="store_true", help="Desactivar RNNoise")
args = parser.parse_args()
ic = Intercom(
playback_device=args.playback_device,
capture_device=args.capture_device,
rx_port=args.rx_port,
tx_ip=args.tx_ip,
tx_port=args.tx_port,
ptt_gpio=args.ptt_gpio,
led_gpio=args.led_gpio,
vox_threshold=args.vox_threshold,
vox_hold_ms=args.vox_hold_ms,
denoise=not args.no_denoise
)
def handle_sigint(signum, frame):
ic.stop()
sys.exit(0)
signal.signal(signal.SIGINT, handle_sigint)
signal.signal(signal.SIGTERM, handle_sigint)
ic.start()
print("Intercom en ejecución. Ctrl+C para salir.")
while True:
time.sleep(1)
if __name__ == "__main__":
main()
Breve explicación de partes clave:
– audio_in_cb: callback que recibe bloques de 10 ms desde ALSA (micrófono I2S). Cada bloque se pasa por RNNoise (si está activo) y, en función de PTT/VOX, se envía por UDP como PCM S16_LE con cabecera de secuencia y timestamp.
– audio_out_worker: hilo que recibe por UDP y escribe directamente en el stream de salida (MAX98357A). Si PTT está activo, silenciamos la reproducción para evitar acople en el mismo nodo.
– FRAME_SAMPLES=480 a 48 kHz: elección estándar para RNNoise y baja latencia.
– Dispositivos ALSA: se pueden seleccionar por nombre/índice; si no se especifican, usa el predeterminado del sistema. Recomendación: forzar por nombre la tarjeta del “googlevoicehat-soundcard”.
Compilación/instalación/ejecución
1) Actualiza el sistema e instala dependencias
sudo apt update
sudo apt full-upgrade -y
sudo reboot
Tras reiniciar:
sudo apt install -y \
build-essential cmake pkg-config git \
python3.11 python3.11-venv python3-dev \
libasound2-dev portaudio19-dev \
librnnoise0 librnnoise-dev \
alsa-utils
Comprueba versiones (aprox. en Raspberry Pi OS Bookworm 64‑bit):
– gcc 12.2.0
– cmake 3.25.1
– ALSA lib 1.2.8
– PortAudio 19.6.0
gcc --version | head -n1
cmake --version | head -n1
aplay --version
2) Crea y activa un entorno virtual de Python 3.11
python3 -m venv ~/venvs/intercom
source ~/venvs/intercom/bin/activate
python --version
Deberías ver “Python 3.11.x”.
Actualiza pip a la versión fijada:
python -m pip install --upgrade pip==24.2
Instala paquetes Python con versiones exactas:
pip install numpy==1.26.4 sounddevice==0.4.6 rnnoise==0.4.1 pyyaml==6.0.1 gpiozero==1.6.2
Verifica:
python -c "import numpy, sounddevice, rnnoise, gpiozero; print(numpy.__version__, sounddevice.__version__)"
3) Verifica que la tarjeta I2S está disponible
Lista capturas y reproducciones:
arecord -l
aplay -l
Debería aparecer una tarjeta asociada a “Google voiceHAT SoundCard” o similar, normalmente como card 0, device 0 (hw:0,0). Si no es card 0, ajusta índices en los comandos de prueba.
Prueba rápida de captura (5 s a 48 kHz mono):
arecord -D hw:0,0 -f S16_LE -c 1 -r 48000 -d 5 test_mic.wav
aplay test_mic.wav
Si escuchas tu voz por el altavoz vía MAX98357A, la ruta I2S funciona.
Ajusta volumen de reproducción con alsamixer:
alsamixer
Selecciona la tarjeta del voiceHAT y sube el volumen Master o PCM según disponibilidad.
4) Descarga el código y ejecútalo
Crea un directorio de trabajo y guarda el script:
mkdir -p ~/projects/i2s-intercom
cd ~/projects/i2s-intercom
nano intercom.py
Pega el código completo mostrado arriba, guarda y sal.
Lista dispositivos por nombre para usar con sounddevice:
python - << 'PY'
import sounddevice as sd
for i,d in enumerate(sd.query_devices()):
print(i, d['name'])
PY
Anota el índice o nombre exacto de:
– Dispositivo de captura (mic I2S, suele ser el mismo “voiceHAT”)
– Dispositivo de reproducción (amp I2S, “voiceHAT”)
Supón que ambos son el dispositivo 0 (ajusta si no lo son).
Prepara dos nodos (dos Raspberry Pi Zero 2 W con el mismo montaje), cada uno conoce la IP del otro:
– Nodo A (IP A): enviará a IP B
– Nodo B (IP B): enviará a IP A
En nodo A:
source ~/venvs/intercom/bin/activate
cd ~/projects/i2s-intercom
python intercom.py --tx-ip <IP_B> --tx-port 6000 --rx-port 6000 --capture-device 0 --playback-device 0 --ptt-gpio 17 --led-gpio 27
En nodo B:
source ~/venvs/intercom/bin/activate
cd ~/projects/i2s-intercom
python intercom.py --tx-ip <IP_A> --tx-port 6000 --rx-port 6000 --capture-device 0 --playback-device 0 --ptt-gpio 17 --led-gpio 27
- Si no tienes el pulsador PTT ni el LED, omite –ptt-gpio y –led-gpio. El script usará VOX (activación por voz) con umbral y hold configurables.
Parámetros útiles:
– –vox-threshold 0.008 … 0.02 según ruido ambiente
– –vox-hold-ms 200 … 600 para no “cortar” sílabas
– –no-denoise para desactivar RNNoise (comparativa A/B)
Validación paso a paso
1) Verificación del hardware I2S
– arecord -l y aplay -l muestran una tarjeta “Google voiceHAT SoundCard” (o similar).
– arecord -D hw:0,0 -f S16_LE -c 1 -r 48000 -d 5 test.wav y aplay test.wav reproducen audio nítido por el altavoz.
2) Nivel y ganancia
– Ejecuta alsamixer y selecciona la tarjeta correcta.
– Asegura que el volumen maestro no está en mute y el nivel de salida esté entre 70–90% para pruebas.
3) Latencia y estabilidad
– En el intercom, habla por el micrófono del nodo A y deberías escucharte en <200–300 ms en el nodo B, dependiendo de la red.
– Ajusta el volumen para evitar acople.
4) Supresión de ruido
– Con ruido de fondo (ventilador, calle), compara:
– Modo con RNNoise (por defecto).
– Modo sin RNNoise: añade “–no-denoise”.
– Deberías percibir atenuación del ruido estacionario (≈ 10–20 dB en frecuencias persistentes) y mejora de inteligibilidad.
5) Semidúplex PTT/VOX
– Con PTT por botón: al pulsar, el LED enciende y se transmite; al soltar, se recibe. Verifica que el altavoz local se silencia en TX.
– Con VOX: ajusta –vox-threshold y –vox-hold-ms. El sistema transmite solo en voz. Habla y observa que la transmisión se corta después del hold.
6) Robustez de UDP
– Simula jitter: si hay pequeñas pérdidas, la reproducción debería seguir sin cortes apreciables gracias a los tamaños pequeños de trama (10 ms).
7) Consumo CPU
– Monitoriza con top/htop; en Zero 2 W, RNNoise a 48 kHz y 10 ms por trama suele ser sostenible. Ajusta prioridad o reduce tasa a 16 kHz si fuera necesario (ver “Mejoras/variantes”).
8) Prueba cruzada de routing
– Cambia los puertos y verifica que los sockets se abren correctamente. Usa netstat -anu o ss -lun para ver puertos en uso.
Troubleshooting
1) No aparece la tarjeta I2S tras el reboot
– Causa: Falta “dtoverlay=googlevoicehat-soundcard” o “dtparam=audio=off”.
– Solución:
– Edita /boot/firmware/config.txt y añade:
– dtparam=audio=off
– dtoverlay=googlevoicehat-soundcard
– Revisa que no tengas overlays conflictivos (otros DAC I2S).
– Reboot.
2) arecord/aplay funcionan pero el script no encuentra el dispositivo
– Causa: sounddevice/PortAudio usa índices distintos.
– Solución:
– Lista dispositivos con el snippet de Python mostrado.
– Pasa –capture-device y –playback-device con el índice o nombre correcto (o usa “hw:CARD=…” si corresponde).
3) Audio con chasquidos o underruns/overruns frecuentes
– Causa: Blocksize no óptimo, CPU al 100%, latencia muy baja para la red.
– Solución:
– Aumenta blocksize (p. ej., duplica FRAME_MS a 20 ms y FRAME_SAMPLES a 960).
– Cierra servicios de fondo pesados. Usa la versión “Lite” del OS.
– Asegura buena alimentación 5 V/2.5 A; evita undervoltage (dmesg | grep -i voltage).
– Revisa cableado I2S y longitudes de cables.
4) Realimentación acústica (acople)
– Causa: el mic capta el altavoz local o remoto.
– Solución:
– Aumenta separación física o baja el volumen en alsamixer.
– Usa PTT (semidúplex) o VOX para no reproducir mientras transmites.
– Encapsula el micrófono en una caja con antiviento o pantalla acústica.
5) El micrófono no capta nada (silencio)
– Causa: SEL mal configurado, DOUT no conectado a GPIO20, mala alimentación del mic (5 V por error).
– Solución:
– Verifica que SEL del SPH0645 está a GND (canal izquierdo).
– Revisa DOUT (mic) -> GPIO20 (PCM_DIN).
– Confirma 3.3 V en el mic, no 5 V.
6) El MAX98357A no suena
– Causa: DIN no conectado a GPIO21, falta BCLK/LRCLK, altavoz mal conectado.
– Solución:
– Verifica GPIO21 (PCM_DOUT) -> DIN del MAX98357A.
– BCLK (GPIO18) y LRCLK (GPIO19) conectados y compartidos.
– Revisa el altavoz (4–8 Ω) y las bornas del módulo.
7) PipeWire/PulseAudio interfieren
– Causa: sesión gráfica con PipeWire tomando control del audio.
– Solución:
– Ejecuta el script especificando dispositivos “hw:…” vía sounddevice para saltar capas.
– Si es necesario, detén servicios de usuario de PipeWire para pruebas:
– systemctl –user stop pipewire pipewire-pulse wireplumber
8) Latencia de red demasiado alta o jitter
– Causa: Wi‑Fi saturado, enlace inestable.
– Solución:
– Usa 5 GHz si es posible en otro modelo (Zero 2 W es 2.4 GHz; aproxima el AP).
– Reduce bitrate con compresión (Opus) o aumenta FRAME_MS a 20 ms.
– Conecta por Ethernet usando un adaptador USB OTG 10/100 (opción avanzada).
Mejoras/variantes
- Compresión Opus
- Reduce el ancho de banda UDP de ~768 kbps PCM 48 kHz mono a 16–32 kbps con códec Opus.
- Paquetes: libopus0, opuslib (pip) o pyogg.
-
Inserta la codificación/decodificación en el pipeline, manteniendo RNNoise antes del codificador.
-
Echo Cancellation (AEC)
- Integra WebRTC Audio Processing (AEC + AGC + NS). En Pi Zero 2 W es posible pero más exigente.
- Paquetes: libwebrtc-audio-processing-dev (compilación), binding Python o C++ embebido.
-
Arquitectura: ruta de referencia de altavoz hacia el AEC y mic como ruta primaria.
-
Resample a 16 kHz
- RNNoise admite entrada 48 kHz, pero puedes resamplear a 16 kHz para reducir CPU y ancho de banda.
-
Usa librosa o soxr (pysoxr) para calidad alta, o implementa un filtro simple para prototipo.
-
Buffer adaptativo y jitter buffer
-
Añade un pequeño jitter buffer con timestamps y reordenamiento por seq para mayor robustez en redes ruidosas.
-
Seguridad y descubrimiento
- Descubrimiento mDNS/Avahi para IPs dinámicas.
-
Cifrado (DTLS/SRTP) si el entorno lo requiere.
-
Supervisión y métricas
-
Exponer métricas (pérdida de paquetes, jitter, latencia) vía Prometheus o logs JSON para diagnóstico.
-
Integración con servicios del sistema
- Crear un servicio systemd para iniciar el intercom al arranque y gestionar reinicios.
Checklist de verificación
- [ ] He instalado Raspberry Pi OS Bookworm 64‑bit y actualizado el sistema.
- [ ] He habilitado el overlay en /boot/firmware/config.txt:
- [ ] dtparam=audio=off
- [ ] dtoverlay=googlevoicehat-soundcard
- [ ] He cableado:
- [ ] 3V3 a SPH0645 (no al MAX98357A)
- [ ] 5V a MAX98357A
- [ ] GND común a todos los módulos
- [ ] GPIO18 (BCLK) a BCLK de mic y amp
- [ ] GPIO19 (LRCLK) a LRCLK/LRC de mic y amp
- [ ] GPIO20 (DIN de Pi) a DOUT del SPH0645
- [ ] GPIO21 (DOUT de Pi) a DIN del MAX98357A
- [ ] SEL del SPH0645 a GND
- [ ] arecord -l y aplay -l muestran la tarjeta I2S (voiceHAT)
- [ ] arecord/aplay de prueba funcionan a 48 kHz mono
- [ ] He creado venv con Python 3.11.2 y pip 24.2
- [ ] He instalado numpy==1.26.4, sounddevice==0.4.6, rnnoise==0.4.1, pyyaml==6.0.1, gpiozero==1.6.2
- [ ] He listado dispositivos con sounddevice y anotado índices correctos
- [ ] He ejecutado intercom.py en ambos nodos con IPs cruzadas
- [ ] PTT funciona (si se usa) y LED indica TX; VOX funciona (si se usa)
- [ ] Noto supresión de ruido perceptible con RNNoise activado
- [ ] Latencia aceptable y sin chasquidos; niveles ajustados en alsamixer
Con este caso práctico, has implementado un intercomunicador full‑duplex con micrófono I2S Adafruit SPH0645 y amplificador I2S MAX98357A sobre Raspberry Pi Zero 2 W, ejecutando supresión de ruido en tiempo real con RNNoise, usando ALSA/PortAudio a 48 kHz para baja latencia. La arquitectura es extensible a compresión Opus, AEC con WebRTC y despliegue como servicio systemd para un intercom profesional y robusto.
Encuentra este producto y/o libros sobre este tema en Amazon
Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.



