¿Alguna vez quisiste escuchar tu propia voz una octava más grave, con un eco de sala vacía encima y sin usar un computador? Eso es exactamente lo que hace el Voicebox FX: un grabador de voz portátil, del porte de la palma de la mano, que captura hasta 20 segundos de audio, lo reproduce en loop y te deja deformarlo en vivo con tres perillas.

Lo interesante no es el juguete, sino lo que demuestra: que un microcontrolador de 10 mil pesos hoy corre una cadena completa de procesamiento de audio. pitch shifting granular, reverb algorítmico y delay. en tiempo real y en Python. Hace tres años eso requería una DSP dedicada.

En esta guía vas a aprender a armar el proyecto portado a la Raspberry Pi Pico 2, que usa el mismo chip RP2350 de la placa original pero cuesta bastante menos y se consigue en Chile. Al terminar vas a saber cómo se encadenan los efectos de audio en CircuitPython, qué pin de la Pico 2 reemplaza a cada pin del diseño original, y cómo alimentar todo con una batería de litio recargable.

Cómo funciona por dentro: la cadena de audio

Antes del hardware conviene entender el flujo de la señal, porque es lo que explica por qué el código se ve como se ve.

El audio entra por un micrófono I2S. I2S es un bus digital de tres líneas (reloj de bits, selector de canal y datos) diseñado específicamente para audio: a diferencia de un micrófono analógico, acá el módulo entrega muestras ya digitalizadas, así que no dependes del ADC del microcontrolador ni del ruido que se cuela por la fuente de alimentación. El micrófono captura a 32 bits y el objeto I2SIn los recorta a 16 bits antes de escribirlos.

Esas muestras no van directo a un archivo: se escriben primero a un io.BytesIO, es decir, a un buffer en RAM. Recién cuando sueltas el botón de grabar, ese buffer se vuelca al sistema de archivos como un .wav. Es un detalle importante. escribir a flash mientras grabas provocaría pausas impredecibles y muestras perdidas, porque una escritura a la memoria flash del microcontrolador bloquea el procesador por milisegundos.

En la reproducción viene la parte elegante. Cada efecto de CircuitPython es a la vez un reproductor y una fuente de audio, así que se encadenan uno dentro del otro:

archivo WAV → PitchShift → Freeverb → Echo → Distortion → Mixer → salida I2S

Cada objeto toma como entrada la salida del anterior. Por eso en el código todos llevan loop=True: si un eslabón de la cadena no está en loop, se corta al primer ciclo y arrastra a todos los que vienen después. Es el error más común al modificar este proyecto.

El PitchShift merece una nota aparte: trabaja con una ventana de 2048 muestras y un solape de 256. Ese solape es lo que evita los clics audibles al empalmar fragmentos reproducidos a otra velocidad. Si lo bajas mucho, la voz suena metálica; si lo subes, aumenta la latencia. Los valores del código están calibrados para 16 kHz mono, así que si cambias la tasa de muestreo tendrás que reajustarlos.

El hardware: qué hace cada módulo

Placa Adafruit Feather RP2350 usada en el proyecto original

El proyecto original usa una Adafruit Feather RP2350 con 8 MB de PSRAM. Nosotros vamos con la Raspberry Pi Pico 2, que monta el mismo microcontrolador RP2350 de doble núcleo. La diferencia real: la Feather trae cargador de batería LiPo y conector JST integrados, y la Pico 2 no. Eso lo resolvemos más abajo con un módulo TP4056.

Módulo micrófono I2S MEMS para captura de audio digital

El micrófono I2S es el corazón de la entrada. El diseño original usa un ICS-43434; en Chile el módulo equivalente y disponible es el INMP441, también MEMS y también I2S, con la misma interfaz de tres líneas. La única diferencia práctica es la justificación de los datos, que se configura por software.

Módulo amplificador I2S clase D MAX98357A

Para la salida se usa un amplificador I2S clase D MAX98357A, que combina conversor digital analógico y amplificador de 3 W en una sola placa. Este es el componente que no tiene equivalente en el catálogo local, así que más abajo te explico qué opciones tienes.

Stick de 8 LEDs RGB direccionables 5050 tipo NeoPixel

Los 8 LEDs RGB direccionables no son decoración: son la interfaz de estado completa del aparato. Rojo significa grabando, amarillo que está escribiendo el archivo, verde reproduciendo y azul un error. Como el dispositivo no tiene pantalla, ese código de colores es toda la retroalimentación que recibes.

Potenciómetro deslizante lineal de 45 mm para control de tono

Los tres controles analógicos se reparten así: el deslizante maneja el pitch (de -13 a +13 semitonos, o sea una octava completa hacia cada lado), y los dos rotatorios controlan la mezcla de reverb y el decaimiento del eco.

Interruptor de palanca SPDT de montaje en panel

Finalmente hay dos interruptores con funciones distintas: uno corta la alimentación y el otro conmuta el modo de escritura del sistema de archivos. Este segundo es clave y suele confundir: CircuitPython no puede escribir en su propia unidad CIRCUITPY mientras el computador la tiene montada. Si el interruptor está en la posición equivocada, el código captura el audio pero falla al guardarlo y los LEDs se ponen azules.

Conexiones: del diseño original a la Pico 2

El cableado original, tal como lo publica Adafruit para la Feather RP2350:

Bloque Conexiones
Micrófono I2S 3V→VDD, GND→GND, D9→DOUT, D5→BCLK, D6→LRCL
Amplificador I2S 3V→VIN, GND→GND, D11→LRC, D10→BCLK, SCL→DIN
LEDs RGB Bat→5VDC, GND→GND, D12→DATAIN
Potenciómetro deslizante 3V→pin 1, A0→pin 2, GND→pin 3
Potenciómetro rotatorio A GND→pin 1, A1→pin 2, 3V→pin 3
Potenciómetro rotatorio B GND→pin 1, A2→pin 2, 3V→pin 3
Botón A (reproducir) A3→pin 1, GND→pin 2
Botón B (grabar) D24→pin 1, GND→pin 2
Interruptor de palanca D25→pin 1, GND→pin 2
Interruptor deslizante GND→pin 1, EN→pin 2

Y esta es la traducción a Raspberry Pi Pico 2, que es la parte que tendrás que adaptar en el código:

Pin original (Feather) Pin en Pico 2 Función
A0 GP26 Deslizante de pitch (ADC0)
A1 GP27 Reverb (ADC1)
A2 GP28 Decaimiento del eco (ADC2)
A3 GP15 Botón de reproducir
D24 GP14 Botón de grabar
D5 / D6 / D9 GP5 / GP6 / GP9 BCLK, LRCL y datos del micrófono
D10 / D11 / SCL GP10 / GP11 / GP13 BCLK, LRC y DIN del amplificador
D12 GP12 Datos de los LEDs RGB
D25 GP16 Interruptor de modo de escritura

Tres detalles que ahorran una tarde de depuración:

El botón de reproducir no necesita un pin analógico. En el diseño original está en A3, pero se usa como entrada digital a través de keypad.Keys. La Pico 2 solo expone tres canales ADC utilizables (GP26, GP27 y GP28) y los tres se van a los potenciómetros, así que el botón puede ir a cualquier GPIO libre.

Alimenta los LEDs desde VSYS, no desde 3V3. Los WS2812 esperan lógica de datos por sobre el 70 % de su tensión de alimentación. Si los alimentas con 5 V y les hablas con 3,3 V, van a parpadear de forma errática. Conectados a VSYS. los 3,7 V que entrega la batería de litio. el margen queda cómodo y funcionan sin conversor de nivel.

El pin SCL del original es solo un GPIO cualquiera. No hay I2C en este proyecto; ese nombre viene del pinout de la Feather. En la Pico 2 usa cualquier pin libre para el DIN del amplificador.

El firmware: CircuitPython en versión de desarrollo

Acá viene la advertencia más importante del proyecto, y conviene leerla antes de comprar nada.

Los módulos audioi2sin, audiofilewriter y GranularPitchShift que usa este código son nuevos. Necesitas CircuitPython 10.3.0.alpha-4 o posterior, que al momento de publicarse la guía original todavía no tenía versión estable. En circuitpython.org tienes que bajar hasta el panel de development release, no el botón grande de descarga.

Para instalarlo en la Pico 2:

  1. Mantén presionado el botón BOOTSEL y, sin soltarlo, conecta el cable USB.
  2. Aparece una unidad llamada RP2350. Suelta el botón.
  3. Arrastra el archivo .uf2 a esa unidad.
  4. La unidad desaparece y aparece otra llamada CIRCUITPY. Listo.

Si la unidad no aparece, revisa el cable: muchos cables USB son solo de carga y no llevan las líneas de datos. Es la causa número uno de placas "muertas" que en realidad están perfectas.

El código

El firmware completo, tal como lo publicó Liz Clark para Adafruit. Recuerda que las referencias a pines corresponden a la Feather RP2350: aplica la tabla de conversión de más arriba antes de cargarlo en una Pico 2.

Python
# SPDX-FileCopyrightText: 2026 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Voice Box FX Changer
I2S Mic in -> .WAV file -> I2S DAC out
Effects controlled with analog inputs during looped playback
"""
import io
import time
import audiobusio
import board
import audioi2sin
import simpleio
import audiodelays
import audiofreeverb
import audiofilewriter
import audiofilters
import audiocore
import audiomixer
import keypad
from analogio import AnalogIn
import neopixel

pitch_slide = AnalogIn(board.A0)
reverb_slide = AnalogIn(board.A1)
dist_slide = AnalogIn(board.A2)
# record button on D24, play button on A3
keys = keypad.Keys((board.D24, board.A3), value_when_pressed=False, pull=True)

pixels = neopixel.NeoPixel(board.D12, 8, brightness=0.6, auto_write=True)
RED = (255, 0, 0)
YELLOW = (255, 255, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
PURPLE = (50, 0, 255)
OFF = (0, 0, 0)

# recording config
SAMPLE_RATE = 16000
OUTPUT_PATH = "/recording.wav"
MAX_RECORD_SECONDS = 20
# mono 16-bit = 2 bytes/sample + room for the 44-byte WAV header
CAPTURE_ALLOC = SAMPLE_RATE * 2 * MAX_RECORD_SECONDS + 64
capture = None

# Mic
mic = audioi2sin.I2SIn(
    bit_clock=board.D5,
    word_select=board.D6,
    data=board.D9,
    sample_rate=SAMPLE_RATE,
    bit_depth=32,
    output_bit_depth=16,
    mono=True,
    left_justified=False, # using ICS43434
)

i2s   = audiobusio.I2SOut(board.D10, board.D11, board.SCL)
mixer = audiomixer.Mixer(
    voice_count=1,
    sample_rate=SAMPLE_RATE,
    channel_count=1,
    bits_per_sample=16, # matches output_bit_depth
    samples_signed=True,
)
i2s.play(mixer)
mixer.voice[0].level = 0.5

pitch_shift = audiodelays.PitchShift(
    semitones=0.0,
    mix=1.0,
    window=2048,
    overlap=256,
    buffer_size=1024,
    channel_count=1,
    sample_rate=SAMPLE_RATE,
)

reverb = audiofreeverb.Freeverb(
    roomsize=0.35,
    damp=0.25,
    buffer_size=1024,
    channel_count=1,
    sample_rate=SAMPLE_RATE,
    mix=0.0,
)

echo = audiodelays.Echo(
    max_delay_ms=1000,
    delay_ms=850,
    decay=0.0,
    buffer_size=1024,
    channel_count=1,
    sample_rate=SAMPLE_RATE,
    mix=1.0,
    freq_shift=False
)

amp = audiofilters.Distortion(
    pre_gain=15,
    drive=0.00,
    mode=audiofilters.DistortionMode.LOFI,
    soft_clip=True,
    mix=1.0,
    buffer_size=1024,
    sample_rate=SAMPLE_RATE,
    bits_per_sample=16,
    samples_signed=True,
    channel_count=1,
)

loop = False
recording = False
file = open("/bootbeeps.wav", "rb")
wav = audiocore.WaveFile(file)
mixer.voice[0].play(wav, loop=False)
# start-up purple marquee
for i in range(8):
    pixels[i] = PURPLE
    time.sleep(0.2)
time.sleep(0.5)
mixer.voice[0].level = 1.0
pixels.fill(OFF)

while True:
    event = keys.events.get()
    if event:
        key_number = event.key_number
        if event.pressed and key_number == 0: # record button
            if not recording: # press to record
                i2s.stop() # stopping i2s out mutes DAC
                pixels.fill(RED) # red means recording
                capture = io.BytesIO(CAPTURE_ALLOC) # write into memory
                writer = audiofilewriter.AudioFileWriter(capture)
                writer.play(mic)
                recording = True
            else: # press to stop
                writer.stop()
                pixels.fill(YELLOW) # yellow while writing
                print("captured", capture.tell(), "bytes")
                try:
                    capture.seek(0) # write from memory to wav on file system
                    with open(OUTPUT_PATH, "wb") as f:
                        while True:
                            chunk = capture.read(4096)
                            if not chunk:
                                break
                            f.write(chunk)
                    capture.seek(0)
                    recording = False
                    pixels.fill(OFF)
                except OSError as e:
                    pixels.fill(BLUE) # error = blue, but continues running
                    print(f"Read-only mode, can't save the file, flip the switch and reboot!: {e}")
                    time.sleep(2)
                    pixels.fill(OFF)
                    continue

        if event.pressed and key_number == 1: # play button
            if not loop: # press to play, looping
                i2s.play(mixer)
                try:
                    # opens wav file that was just written from memory
                    file = open("recording.wav", "rb")
                    wav = audiocore.WaveFile(file)
                    print("got file")
                    pixels.fill(GREEN) # playback is green
                except AttributeError as e:
                    pixels.fill(BLUE)
                    print(f"Missing recording.wav: {e}")
                    time.sleep(2)
                    pixels.fill(OFF)
                    continue
                loop = True
                pitch_shift.play(wav, loop=True) # effect chain, all have to loop
                reverb.play(pitch_shift, loop=True)
                echo.play(reverb, loop=True)
                amp.play(echo, loop=True) # gain boost
                mixer.voice[0].play(amp, loop=True)
            else: # press to stop
                mixer.voice[0].stop()
                loop = False
                pixels.fill(OFF)
    # controlling % of reverb in the mix
    verb = simpleio.map_range(reverb_slide.value, 100, 65536, 0.0, 1.0)
    reverb.mix = verb
    # controlling amount of echo decay
    echo_range = simpleio.map_range(dist_slide.value, 100, 65536, 0.0, 1.0)
    echo.decay = echo_range
    # an octave has 12 semitones, -13 and +13 gives full octave up and down
    pitch = simpleio.map_range(pitch_slide.value, 100, 65536, -13.0, 13.0)
    pitch_shift.semitones = int(pitch)

Junto al code.py necesitas dos cosas en la unidad CIRCUITPY: la carpeta lib con las librerías neopixel, simpleio, adafruit_pixelbuf y keypad, y un archivo bootbeeps.wav que es el sonido de arranque. Ambos vienen en el Project Bundle enlazado al final.

La carcasa impresa en 3D

Carcasa impresa en 3D del grabador de voz portátil

Las piezas vienen en formato 3MF, ya orientadas para imprimir en FDM con PLA. Requisitos concretos:

  • Volumen de impresión mínimo: 80 × 130 × 16 mm. Cualquier impresora doméstica lo cumple.
  • El difusor de los LEDs va en PLA blanco o transparente. Si lo imprimes en un color opaco no vas a ver el código de colores, que es toda la interfaz del aparato.
  • La tapa superior está diseñada para impresoras multicolor tipo AMS, pero el ZIP incluye versiones de un solo color de las perillas y las teclas.

El reparto de componentes en la carcasa: en la mitad inferior van la placa, el amplificador, el micrófono, la batería y el stick de LEDs; en la superior, los dos potenciómetros rotatorios, el deslizante, los botones y el parlante.

Para el armado se usan tornillos M2.5 × 4 mm y 6 mm, M2 × 4 mm y 6 mm, y M3 × 6 mm.

Pruebas y errores comunes

Cuando cargues el código por primera vez deberías ver los 8 LEDs encendiéndose en morado uno por uno mientras suena el beep de arranque. Si eso pasa, el firmware, los LEDs y la salida de audio están funcionando.

Los LEDs se ponen azules al detener la grabación. El sistema de archivos está en modo de solo lectura. Cambia el interruptor de modo, reinicia la placa y vuelve a intentar. El propio código lo dice en el mensaje de error.

Se graba pero no se escucha nada al reproducir. Fíjate en i2s.stop() dentro del bloque de grabación: detener la salida I2S silencia el amplificador a propósito, para que el micrófono no capture su propio parlante. Si el audio no vuelve al reproducir, es porque i2s.play(mixer) no se está ejecutando.

La voz suena entrecortada o con clics. Casi siempre es memoria. Veinte segundos a 16 kHz en 16 bits mono son unos 640 KB solo del buffer de captura, y encima corren cuatro efectos con buffers de 1024 muestras cada uno. La Pico 2 tiene 520 KB de SRAM contra los 8 MB de PSRAM de la Feather original: baja MAX_RECORD_SECONDS a 5 u 8 segundos en la Pico 2, o el io.BytesIO no alcanza a reservarse.

El pitch no llega al extremo. El rango del map_range empieza en 100, no en 0, para descartar el ruido de fondo del ADC. Es intencional: un potenciómetro real nunca entrega exactamente 0.

Variantes y mejoras

Guarda varias tomas en lugar de una. El código escribe siempre a /recording.wav y pisa la anterior. Si conectas un módulo lector de microSD por SPI y numeras los archivos, conviertes el aparato en una grabadora de campo con banco de muestras.

Agrega un segundo botón para el modo "loop pedal". Con la cadena de efectos ya montada, superponer una nueva grabación sobre la que está sonando es cuestión de agregar una segunda voz al Mixer — el objeto ya soporta voice_count mayor que 1.

Convierte el color de los LEDs en un VU meter. En vez de usar los 8 LEDs solo como semáforo de estado, puedes mapear el nivel de la señal a la cantidad de LEDs encendidos durante la reproducción. Es puramente software y le da una segunda lectura visual al aparato.

Súbelo a una placa con más RAM. Si los 20 segundos completos te importan más que el precio, una placa RP2350 con PSRAM externa recupera la duración original sin tocar el código.

Personalización para Chile

Esta es la lista de compras real para armar el Voicebox FX en Chile, con la Raspberry Pi Pico 2 en reemplazo de la Feather RP2350:

  • Raspberry Pi Pico 2 (SKU N-340). $9.990 CLP. Mismo chip RP2350 que la placa original.
  • Módulo micrófono I2S INMP441 (SKU GL3-12). $5.890 CLP. Equivalente directo del ICS-43434.
  • Módulo RGB LED 8 bits 5050 (SKU GN3-11). $1.990 CLP. Es el mismo stick de 8 WS2812 del original.
  • Resistencia variable lineal 10K deslizante (SKU GD3-11). $2.390 CLP.
  • Potenciómetro rotacional lineal 10K (SKU GB1-5). $690 CLP c/u, necesitas 2.
  • Parlante altavoz 8 Ohm 1W 50mm (SKU G-107). $3.290 CLP.
  • Pila 18650 3000mAh con punta (SKU L-040). $5.190 CLP. Reemplaza la celda de 2200 mAh del original con más autonomía.
  • Porta batería 18650 x1 (SKU L-325). $1.390 CLP.
  • TP4056 controlador de carga USB-C (SKU GQ2-12). $1.990 CLP. Suple el cargador que la Feather trae integrado y la Pico 2 no.
  • Interruptor de palanca conmutador ON ON 3 pines (SKU GB1-16). $890 CLP.
  • Mini switch interruptor proto 3 pines (SKU GF1-14). $290 CLP.
  • Botón pulsador 12x12x8mm 4 pines (SKU GA6-16). $290 CLP c/u, necesitas 2. El original usa teclas mecánicas Kailh CHOC, que son más lindas pero eléctricamente hacen lo mismo.
  • Filamento PLA SunLu 1.75mm blanco (SKU SL-003). $13.990 CLP el kilo. El blanco sirve además para el difusor de los LEDs.
  • Cable UL1007 26AWG por metro (SKU V-121V1). $500 CLP el metro. Necesitas unos 3 metros en distintos colores.

El componente que no tenemos: el amplificador I2S MAX98357A no está en el catálogo, y no corresponde reemplazarlo por un PAM8403 o un LM386 porque esos son amplificadores analógicos. esperan una señal de audio, no un bus digital I2S. El RP2350 en este proyecto entrega I2S puro, así que el MAX98357A hay que importarlo o conseguirlo por separado. La alternativa honesta, si prefieres comprar todo localmente, es reescribir la salida usando audiopwmio.PWMAudioOut de CircuitPython hacia un PAM8403: pierdes calidad de audio y ganas ruido de conmutación, pero el proyecto funciona.

Costo aproximado del resto del BOM en MechatronicStore: alrededor de $50.000 CLP, contando el kilo completo de filamento (del que vas a usar unos 150 gramos).

Recursos

Versión chilena inspirada en el proyecto original de Adafruit, con componentes en stock local en MechatronicStore.