You dont have javascript enabled! Please enable it!

Caso práctico: Intercomunicador I2S con supresión de ruido

Caso práctico: Intercomunicador I2S con supresión de ruido — hero

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

Ir a 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.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo requerido para el proyecto?




Pregunta 2: ¿Qué versión de Python se debe utilizar?




Pregunta 3: ¿Qué hardware específico se necesita para el proyecto?




Pregunta 4: ¿Qué amplificador se menciona en los requisitos?




Pregunta 5: ¿Cuál es la versión de CMake requerida?




Pregunta 6: ¿Qué paquete de Python es opcional para PTT por GPIO?




Pregunta 7: ¿Qué herramienta se utiliza para la gestión de audio de baja latencia?




Pregunta 8: ¿Qué versión de pip se debe actualizar explícitamente?




Pregunta 9: ¿Qué micrófono se utiliza en el proyecto?




Pregunta 10: ¿Qué versión del kernel de Linux se requiere?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio