Grabar audio con un microcontrolador siempre fue un dolor de cabeza: micrófonos analógicos con ruido, ADC saturados y librerías a medio terminar. Eso cambió. Desde CircuitPython 10.3.0-alpha.3 existe el módulo audioi2sin, que deja usar micrófonos digitales I2S como el ICS-43434 o el SPH0645LM4H directamente desde Python, sobre placas Raspberry Pi (RP2350) y Espressif.

En esta guía vas a montar tres proyectos con el mismo micrófono I2S: grabar audio a un archivo WAV en tarjeta microSD, hacer NeoPixels que reaccionan al sonido, y. como idea avanzada. separar frecuencias con FFT. Al terminar vas a entender por qué un micrófono I2S de 24 bits viaja en "ranuras" de 32 bits y cómo convertir eso a PCM de 16 bits.

Por qué I2S y no un micrófono analógico

Un micrófono MEMS I2S entrega la señal ya digitalizada por un bus serial de tres líneas (reloj de bits, selección de palabra y datos). Eso significa cero ruido analógico en el cable y nada de calibrar un ADC. La contraparte: el dato llega en un formato particular. Estos micrófonos son de 24 bits pero cada muestra "viaja" dentro de una ranura de 32 bits, así que en el código vas a ver desplazamientos de bits (>> 16 o >> 8) para quedarte con los bits altos, que ya son una representación PCM utilizable.

Placa Metro RP2350 con PSRAM para grabación de audio

Módulo micrófono MEMS I2S ICS-43434

Proyecto 1: grabar 10 segundos a un WAV en microSD

Este ejemplo graba 10 segundos de audio a RAM y después los escribe a un archivo .wav en la microSD. Corre en una Metro RP2350 con PSRAM: esa RAM extra es la que permite bufferear los 10 segundos completos en memoria antes de volcarlos a la tarjeta.

Necesitas CircuitPython 10.3.0-alpha.3 o más nuevo (bájalo de la página de descargas de la Metro RP2350). El cableado entre la Metro y el breakout del micrófono es directo:

  • 3V Metro → 3V breakout ICS-43434
  • GND Metro → GND breakout
  • D5 Metro → BCLK (reloj de bits)
  • D6 Metro → LRCL (word select)
  • D9 Metro → DOUT (salida de datos)

Copia el code.py a la unidad CIRCUITPY que aparece al conectar la placa, inserta una microSD formateada en FAT y reinicia con el botón reset. Al ejecutarse, el código te pide presionar Enter en el terminal serial para empezar a grabar:

Python
# SPDX-FileCopyrightText: 2026 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""Basic wave file recording example for Metro RP2350 w/ PSRAM.

Records 10 seconds of audio to RAM then writes it to a .wav
file on the sd card.

Must use Metro RP2350 with PSRAM for 10 second recording.

Wiring:
  bclk          -> board.D5
  word select   -> board.D6
  data          -> board.D9

"""
import array
import struct
import time

import board
import audioi2sin


input("press enter to start recording")

#  Recording config
SAMPLE_RATE = 16000
RECORD_SECONDS = 10
CHUNK_SAMPLES = 1024  # samples captured per record() call
OUTPUT_PATH = "/sd/recording.wav"

# Mount SD: Uncomment for devices without builtin SDCard auto-mounting.

# spi = busio.SPI(board.SD_SCK, board.SD_MOSI, board.SD_MISO)
# sdcard = sdcardio.SDCard(spi, cs=board.SD_CARD_DETECT, baudrate=24_000_000)
# vfs = storage.VfsFat(sdcard)
# storage.mount(vfs, "/sd")

#  Mic
# 24-bit MEMS mics ride in 32-bit slots. Downconvert each slot to a
# signed 16-bit PCM sample before writing.
mic = audioi2sin.I2SIn(
    bit_clock=board.D5,
    word_select=board.D6,
    data=board.D9,
    sample_rate=SAMPLE_RATE,
    bit_depth=32,
    mono=True,
    left_justified=False,  # True for SPH0645LM4H
)

actual_rate = mic.sample_rate
print("Recording at", actual_rate, "Hz for", RECORD_SECONDS, "s ->", OUTPUT_PATH)

total_samples = actual_rate * RECORD_SECONDS

# Per-call scratch buffer for the raw 32-bit slots.
raw = array.array("i", [0] * CHUNK_SAMPLES)

# Whole recording buffered in RAM as signed int16 PCM. At 16 kHz mono this is
# ~2 bytes/sample (e.g. 2 s -> 64 KB). Increase RAM headroom or RECORD_SECONDS
# only as far as available memory allows.
pcm16 = array.array("h", [0] * total_samples)


def write_wav_header(file, sample_rate, num_samples, bits_per_sample=16, channels=1):
    byte_rate = sample_rate * channels * bits_per_sample // 8
    block_align = channels * bits_per_sample // 8
    data_size = num_samples * block_align
    file.write(b"RIFF")
    file.write(struct.pack("<I", 36 + data_size))
    file.write(b"WAVE")
    file.write(b"fmt ")
    file.write(
        struct.pack(
            "<IHHIIHH",
            16,  # fmt chunk size
            1,  # PCM
            channels,
            sample_rate,
            byte_rate,
            block_align,
            bits_per_sample,
        )
    )
    file.write(b"data")
    file.write(struct.pack("<I", data_size))


written = 0
start = time.monotonic()

while written < total_samples:
    # Don't overrun the RAM buffer on the final partial chunk.
    request = min(CHUNK_SAMPLES, total_samples - written)
    n = mic.record(raw, request)
    # Convert 24-bit-in-32-bit slots to signed int16. Top 16 bits of each
    # slot are the high bits of the signed 24-bit sample, which is already
    # a serviceable 16-bit PCM representation.
    for i in range(n):
        pcm16[written + i] = raw[i] >> 16
    written += n

elapsed = time.monotonic() - start
print("Captured", written, "samples in", round(elapsed, 2), "s; writing to SD...")
#  Flush the whole capture to SD at once -
with open(OUTPUT_PATH, "wb") as f:
    write_wav_header(f, actual_rate, written)
    f.write(memoryview(pcm16)[:written])

print("Done. Wrote", written, "samples ->", OUTPUT_PATH)

while True:
    pass

Después de grabar, reinicia la Metro con reset para que el computador refresque la lista de archivos de la microSD; ahí vas a encontrar recording.wav.

Proyecto 2: NeoPixels que reaccionan al sonido

Acá el micrófono I2S alimenta una barra de sonido tipo VU meter arcoíris: mientras más fuerte el sonido, más píxeles se encienden. El truco interesante del código es que calcula un piso de ruido y un peak que se adaptan solos a la sala (auto ganancia), así que no tienes que calibrar nada según el ambiente.

Tira NeoPixel para efectos de audio reactivo

Cableado de la tira NeoPixel a la placa (pines de ejemplo para Sparkle Motion; ajusta según tu placa):

  • 5V5V de la tira NeoPixel
  • GNDGND de la tira
  • D19DIN de la tira

Si tu tira no tiene 30 píxeles, cambia el valor de NUM_PIXELS en el código para que calce con tu largo:

Python
# SPDX-FileCopyrightText: 2026 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""Audio reactive NeoPixels for SparkleMotion.

The louder the sound picked up by the microphone, the more NeoPixels will turn on.

Wiring:
  LED strip   -> board.D19

"""

import array
import math
import time

import board
from audioi2sin import I2SIn
import neopixel

NUM_PIXELS = 30

# uncomment the line matching your device type
PIXEL_PIN = board.D19  # Pin D19 on Sparkle Motion
# PIXEL_PIN = board.D22 # Pin D22 on Sparkle Motion Stick
# PIXEL_PIN = board.D32 # Pin D32 on Mini Sparkle Motion

SAMPLE_RATE = 16000
SAMPLES_PER_FRAME = 512  # ~32 ms windows

pixels = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, brightness=0.3, auto_write=False)

mic = I2SIn(
    bit_clock=board.D26,
    word_select=board.D33,
    data=board.D25,
    sample_rate=SAMPLE_RATE,
    bit_depth=32,
    mono=True,
    left_justified=False,
)

buf = array.array("i", [0] * SAMPLES_PER_FRAME)


def to_signed24(u32):
    return u32 >> 8


def wheel(pos):
    pos = pos % 256
    if pos < 85:
        return (pos * 3, 255 - pos * 3, 0)
    if pos < 170:
        pos -= 85
        return (255 - pos * 3, 0, pos * 3)
    pos -= 170
    return (0, pos * 3, 255 - pos * 3)


# Smoothed noise floor + peak so the effect adapts to the room.
noise_floor = 2000.0
peak = 20000.0
hue = 0
smoothed_level = 0.0

while True:
    mic.record(buf, len(buf))

    # Compute RMS of the window.
    acc = 0
    for raw in buf:
        s = to_signed24(raw)
        acc += s * s
    rms = math.sqrt(acc / len(buf))

    # Track a slow noise floor and a decaying peak for auto-gain.
    noise_floor = 0.995 * noise_floor + 0.005 * rms
    if rms > peak:
        peak = rms
    else:
        peak *= 0.995
    peak = max(peak, noise_floor + 1000)

    level = (rms - noise_floor) / (peak - noise_floor)
    if level < 0:
        level = 0.0
    elif level > 1:
        level = 1.0

    # Smooth the bar so it doesn't jitter on every frame.
    smoothed_level = 0.6 * smoothed_level + 0.4 * level

    lit = int(smoothed_level * NUM_PIXELS)
    hue = (hue + 2) % 256

    for i in range(NUM_PIXELS):
        if i < lit:
            r, g, b = wheel((hue + i * (256 // NUM_PIXELS)) % 256)
            pixels[i] = (r, g, b)
        else:
            pixels[i] = (0, 0, 0)
    pixels.show()

    time.sleep(0.005)

Fíjate cómo el RMS de cada ventana de muestras se normaliza entre el piso de ruido y el peak, y cómo smoothed_level suaviza la barra para que no tiemble en cada frame. Ese es el tipo de detalle que separa un efecto que se ve profesional de uno que parpadea feo.

Variantes y mejoras

  • Guardar en vez de mostrar: combina el proyecto 1 y el 2 para grabar clips solo cuando el volumen supera un umbral (un grabador activado por voz). Usa la lógica de noise_floor del proyecto 2 como disparador.
  • Detección de aplausos (clap switch): en vez de mover LEDs, cuenta peaks de RMS en una ventana de tiempo y usa dos aplausos seguidos para prender o apagar un relé. Es el "hola mundo" de la domótica por sonido.
  • Análisis de frecuencias con FFT: el ejemplo avanzado del proyecto original usa el módulo ulab para separar graves, medios y agudos en tres tiras distintas. Es la base para un visualizador de espectro o para detectar tonos específicos (por ejemplo, el pitido de un electrodoméstico).

Personalización para Chile

Este proyecto usa hardware bastante específico de Adafruit (Metro RP2350, breakout ICS-43434). En MechatronicStore encuentras los componentes de propósito general que sí son intercambiables:

  • Placa con RP2040/RP2350 o ESP32 compatible con CircuitPython. cualquiera con soporte para audioi2sin y suficiente RAM sirve como cerebro; para grabaciones largas prioriza modelos con PSRAM.
  • Módulo micrófono I2S MEMS (INMP441 es el equivalente más común en el mercado local y usa el mismo bus I2S que el ICS-43434).
  • Tira o anillo NeoPixel WS2812B para la parte de audio reactivo.
  • Módulo lector microSD (SPI) si tu placa no trae ranura integrada.
  • Cable USB-A a USB-C para conectar la placa al computador.

Nota honesta: el INMP441 es un micrófono I2S distinto al ICS-43434 del tutorial; el protocolo I2S es el mismo, pero revisa el parámetro left_justified en el constructor I2SIn, que cambia según el modelo de micrófono.

Recursos

  • Tutorial original (inglés): I2S Microphones with CircuitPython
  • Código de ejemplo: disponible como "Download Project Bundle" y en el GitHub de Adafruit Learning System
  • Documentación: módulo audioi2sin y ulab (FFT) en la documentación de CircuitPython

Proyecto inspirado en la guía de Tim Cocks para Adafruit. Versión chilena con componentes en stock local en MechatronicStore.