Todo sobre la salida de sonido con Pico W

Sonido en el Pico W Título Imagen

La Raspberry Pi Pico W es una potente placa microcontroladora construida alrededor del propio chip microcontrolador de la Raspberry Pi, el RP2040. Puedes consultar una selección de placas de microcontroladores basadas en el RP2040 aquí o sigue leyendo para descubrir todo lo que necesitas saber sobre la salida de sonido con el Pico W.

¿Alguna vez has pensado en hacer un reproductor de música, crear un analizador de espectro de audio o construir un beatbox, pero te has encontrado con problemas para reproducir audio con tu microcontrolador? Pues no busques más, ¡este es tu artículo!

Aquí le presentaré el universo de la modulación por ancho de pulsos (PWM), los protocolos de comunicación para el Pico W y cómo aprovechar la conectividad inalámbrica del Pico W.

Pico W Imagen

Modulación por ancho de pulsos

Una forma de reproducir sonido en el Pico W (o en cualquier otro microcontrolador) es mediante la modulación por ancho de pulsos (PWM).

PWM es una técnica utilizada para controlar dispositivos analógicos utilizando una señal digital. Aunque la salida digital de un microcontrolador sólo puede estar encendida o apagada (0 ó 1), con PWM podemos imitar una señal analógica. (Vale la pena mencionar que PWM no es una verdadera señal analógica.) PWM logra esto cambiando rápidamente el estado de encendido entre encendido y apagado y así controlar la corriente.

Aquí encontrará más información sobre PWM.

Y echa un vistazo a nuestro vídeo:

Como el PWM es súper eficiente, pierde muy poca potencia y es extremadamente preciso, es la técnica que se utiliza en muchas aplicaciones para crear diferentes efectos de audio.

Todos los pines del Pico W tienen capacidad PWM. Es sólo cuestión de codificación.

Permítanme repasar un ejemplo para que puedan ver PWM en acción.

Proyecto

Para demostrar la generación de audio con PWM desde el Pico W, utilicé 2 cables cocodrilo con extremos macho, una protoboard y auriculares.

Como alternativa, si quieres evitar las soldaduras, puedes probar a pinzar el otro extremo de los cables cocodrilo directamente en los auriculares o en la clavija estéreo (Recomiendo encarecidamente no utilizar auriculares para proteger los tímpanos.).

Un Pico W conectado a unos auriculares

Enchufé el cable cocodrilo rojo en el pin 23, que es un pin de tierra (cualquier pin de tierra sirve). Y el PWM se puede generar a través de cualquiera de los pines GP. Como se puede ver en la foto de arriba, estoy usando GP0.

Cualquier juego de auriculares o clavija estéreo moderno tiene tres o cuatro secciones: las dos exteriores son el audio izquierdo y el audio derecho. Por tanto, he conectado el cable cocodrilo rojo a la izquierda y el cable cocodrilo negro a la derecha.

Si sigues esta configuración, asegúrate de que los cables no se toquen.

Otra imagen del Pico W conectado a unos auriculares

Una vez que hayas terminado con la configuración física, el siguiente paso es descargar los archivos de audio.

Si utilizas archivos .wav, asegúrate de que cumplen las siguientes especificaciones:

1. canal: mono (no estéreo)

2. tasa de bits: 22 kHz o inferior

3. muestreo: 16 bits

Aquí hay un ejemplo de código para generar audio con .wav usando CircuitPython:

# PiCockpit.com
# audio output via digital PWM

import board
# loads a wav file for audio playback
from audiocore import WaveFile
# the audiopwmio module contains classes to provide access to audio ID
from audiopwmio import PWMAudioOut

# outputs an analog audio signal by varying the PWM duty cycle under the hood
audio_out = PWMAudioOut(board.GP0)

def play_wave(filename):
    with open(filename, "rb") as wave_file:
        sample = WaveFile(wave_file)
        audio_out.play(sample, loop=True)
# outputs an analog audio signal by varying the PWM duty cycle under the hood
audio_out = PWMAudioOut(board.GP0)

def play_wave(filename):
    with open(filename, "rb") as wave_file:
        sample = WaveFile(wave_file)
        audio_out.play(sample, loop=True)

        while audio_out.playing:
            pass

# audio will loop until program interruption
play_wave("meditation-music.wav")

Si utiliza archivos .mp3, que deben cumplir las mismas especificaciones que los archivos .wav, puede consultar este ejemplo de código:

# PiCockpit.com
# audio output via digital PWM

import board
# loads an mp3 file for audio playback
from audiomp3 import MP3Decoder
# the audiopwmio module contains classes to provide access to audio IO
from audiopwmio import PWMAudioOut

# outputs an analog audio signal by varying the PWM duty cycle under the hood
audio_out = PWMAudioOut(board.GP0)

mp3 = MP3Decoder("meditation-music.mp3")

audio_out.play(mp3)
while audio_out.playing:
    pass

Otra cosa a la que hay que prestar atención con los archivos .mp3 es la velocidad de bits constante. Si el audio suena distorsionado, prueba a bajar más la frecuencia de muestreo y la tasa de bits. La tasa de bits más alta que pude conseguir fue de 192 kbps.

Si estás usando MicroPython en lugar de CircuitPython, puedes recurrir al siguiente ejemplo de código, que genera 3 tonos distintos con PWM estableciendo el ciclo de trabajo y variando la frecuencia.

# PiCockpit.com

from machine import Pin, PWM
from utime import sleep

# lower right corner with USB connector on top
SPEAKER_PIN = 16

# create a PWM object on this pin
speaker = PWM(Pin(SPEAKER_PIN))

# the time each tone will be on
ON_TIME = .25
# the time between tones
OFF_TIME = .1

# low tone
speaker.duty_u16(1000)
speaker.freq(300)
sleep(ON_TIME)
speaker.duty_u16(0)
sleep(OFF_TIME)

# high tone
speaker.duty_u16(1000)
speaker.freq(800)
sleep(ON_TIME)
speaker.duty_u16(0)
sleep(OFF_TIME)

# medium tone
speaker.duty_u16(1000)
speaker.freq(400)
sleep(ON_TIME)

# turn off the PWM
speaker.duty_u16(0)

Protocolos de comunicación

Para que el Pico W y las piezas externas se comuniquen entre sí, existen varios protocolos diferentes. Si alguna vez ha trabajado con la biblioteca de máquinas de MicroPython, sabrá que el protocolo de comunicación estándar para los pines GPIO del Pico W es la biblioteca GPIOZero, que proporciona una interfaz sencilla para controlar y supervisar los pines GPIO. Además, el Pico W también admite otros protocolos de comunicación.

Aquí, quiero hablar un poco sobre UART, SPI, I2C, y I2S, ya que todos ofrecen ciertas ventajas y desventajas en el trabajo con audio en el Pico W.

En primer lugar, el protocolo Universal Asynchronous Receiver/Transmitter, o UART, es teóricamente sencillo de implementar, sólo requiere 2 cables y es muy fiable. Sin embargo, UART tiene un alcance limitado, un ancho de banda limitado y sólo puede soportar la comunicación con otro dispositivo.

Además, a lo largo de mi investigación, he descubierto que UART no es adecuado para reproducir sonido en el Pico W. Si has utilizado UART en el Pico W para la salida de audio, ¡házmelo saber en los comentarios!

SPI

Mientras que UART no cumple con su cometido, la Interfaz Periférica Serie, o SPI, ofrece altas velocidades de transferencia de datos y es extremadamente fiable. Sus mayores desventajas giran en torno al hecho de que SPI sólo permite un controlador y utiliza 4 cables. Pude hacerlo funcionar usando el siguiente código:

# PiCockpit.com
# registers addresses for the PCM5102A module
REG_POWER_CONTROL = 0x02
REG_MODE_CONTROL = 0x03
REG_DAC_CONTROL = 0x0A
REG_DAC_VOLUME = 0x0B

# powers on the PCM5102A module
cs_pin.value(0)
spi.write(bytes([REG_POWER_CONTROL, 0x08]))
cs_pin.value(1)

# sets the mode to I2S
cs_pin.value(0)
spi.write(bytes([REG_MODE_CONTROL, 0x04]))
cs_pin.value(1)

# sets the DAC control bits
cs_pin.value(0)
spi.write(bytes([REG_DAC_CONTROL, 0x00]))
cs_spin.value(1)

# sets the DAC volume
cs_pin.value(0)
spi.write(bytes([REG_DAC_VOLUME, 0x88]))
cs_pin.value(1)

# generates a sine save for testing
sample_rate = 44100
frequency = 440
duration = 5
samples = int(sample_rate * duration)
amplitude = 0x7FFF
sine_wave = bytearray(samplex * 2)
for i in range(samples):
    value = int(amplitude * math.sin(2 * math.pi * frequency * i / sample_rate))
    sine_wave[i*2] = value & 0xFF
    sine_wave[i*2+1] = (value >> 8) & 0xFF

# writes the sine wave to the PCM5102A module
cs_pin.value(0)
spi.write(sine_wave)
cs_pin.value(1)

# Waits for the sound to finish playing
utime.sleep(duration)

# Powers off the PCM5102A module
cs_pin.value(0)
spi.write(bytes([REG_POWER_CONTROL, 0x00]))
cs_pin.value(1)

I2C

El tercer protocolo que quiero tocar es el Circuito de Inter-Integración, o I2C, que sólo necesita 2 cables es simple de implementar, y requiere baja potencia. Al igual que UART, I2C tiene un alcance limitado y un ancho de banda limitado, pero a diferencia de UART, I2C puede comunicarse con múltiples dispositivos. Lo más importante, sin embargo, es que soy capaz de conseguir que I2C funcione con el siguiente código:

# PiCockpit.com

import machine
import utime

# I2C bus
i2c = machine.I2C(0, sda=machine.Pin(0), scl=machine.Pin(1), freq=400000)

# I2C address of the PCM5102A module
i2c_address = 0x4C

# register addresses for the PCM5102A module
REG_POWER_CONTROL = 0x02
REG_MODE_CONTROL = 0x03
REG_DAC_CONTROL = 0x0A
REG_DAC_VOLUME = 0x0B

# powers on the PCM5102A module
i2c.writeto_mem(i2c_address, REG_POWER_CONTROL, b'\x08')

# sets the mode to I2S
i2c.writeto_mem(i2c_address, REG_MODE_CONTROL, b'\x04')

# sets the DAC controls
i2c.writeto_mem(i2c_address, REG_DAC_CONTROL, b'\x00')

# sets the DAC volume
i2c.writeto_mem(i2c_address, REG_DAC_VOLUME, b'\x88')

# generates a sine wave to test
sample_rate = 44100
frequency = 440
duration = 5
samples = int(sample_rate * duration)
amplitude = 0x7FFF
sine_wave = bytearray(samples * 2)
for i in range(samples):
    value = int(amplitude * math.sin(2 * math.pi * frequency * i / sample_rate))
    sine_wave[i*2] = value & 0xFF
    sine_wave[i*2+1] = (value >> 8) & 0xFF

# writes the sine wave to the PCM5102A module
i2c.writeto(i2c_address, sine_wave)

# waits for the sound to finish playing
utime.sleep(duration)

# power of the PCM5102A module
i2c.writeto_mem(i2c_address, REG_POWER_CONTROL, b'\x00')

Teniendo en cuenta que I2C es menos complejo y admite varios dispositivos en un único bus, probablemente sea la mejor opción para aplicaciones de audio más sencillas que requieran transferencias de datos más bajas. Sin embargo, SPI puede ser más adecuado para la reproducción de audio de mayor calidad, ya que soporta velocidades de transferencia de datos más rápidas.

I2S

El cuarto y último protocolo que quiero comentar es Inter-Integrated Sound, o I2S, un protocolo optimizado para el transporte de datos de audio. I2S es, con diferencia, el protocolo ideal para generar audio desde el Pico W, porque proporciona audio de alta fidelidad, transfiere datos rápidamente, es extremadamente fiable y es fácil de usar. Además, existe una amplia selección de componentes de hardware compatibles con I2S, tales como el Pimoroni Pico Audio Pack.

Pimoroni Pico Audio Pack

Para escuchar cómo suena I2S, tengo que cambiar de MicroPython a CircuitPython, ya que I2S aún no está soportado en MicroPython. Pero puedes empujar suavemente el Pico Audio Pack sobre los cabezales del Pico W, asegurándote de que el lado 'USB' del Audio Pack está orientado en la dirección del micro USB del Pico W.

Conecta tus auriculares u otro dispositivo que desees, ¡y ya está!

Una cosa más: puede que quieras bajar un poco el volumen (¡más tarde me lo agradecerás!).

Y aquí está el código de muestra:

# PiCockpit.com

import audiobusio
import audiocore
import board
import array
import time
import math

# sets up I2S, pointing at the correct pins
i2s = audiobusio.I2SOut(board.GP10, board.GP11, board.GP9)

# generates one period of sine wave
# calculates sine wave's length with floor division based on the sample rate and frequency
length = 8000 // 400

# creates an array of signed 16-bit integers based on the length
sine_wave =array.array("H", [0] * length)

for i in range(length):
# calculates sample value with the sin function
    sine_wave[i] = int(math.sin(math.pi * 2 * i / length) * (2 ** 14) + 2 ** 14)

# creates a raw audio sample buffer in memory using the audiocore library
sine_wave = audiocore.RawSample(sine_wave, sample_rate=8000)

i2s.play(sine_wave, loop=True)
time.sleep(1)
i2s.stop()

Wi-Fi

Ya que el Pico W está mostrando capacidades inalámbricas, permítanme mostrarles algo que hace uso de la conectividad inalámbrica.

Este código toma un .mp3 de una URL y lo reproduce desde un archivo una vez finalizada la descarga:

# PiCockpit.com

import adafruit_requests
import wifi
import socketpool
import ssl
import board
import audiomp3
import audiobusio
import os

# sets up I2S for sound output
audio = audiobusio.I2SOut(board.GP10, board.GP11, board.GP9)

# url for the mp3 file to download, 1 minute mp3
mp3_url = 'https://codeskulptor-demos.commondatastorage.googleapis.com/descent/background%20music.mp3'

# chunk size for downloading the mp3 file in parts
chunk_size = 1024

# connects to SSID
wifi.radio.connect(os.getenv('WIFI_SSID'), os.getenv('WIFI_PASSWORD'))

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())

# requests the url and writes the mp3 to a file
try:
    doc = requests.get(mp3_url)
    # opens the file in binary write mode
    with open('sound.mp3', 'wb') as f:
    # writes the file in chunks
    for chunk in doc.iter_content(chunk_size=chunk_size):
        f.write(chunk)
except Exception as e:
    print("Connection error:\n", str(e))

# plays the mp3 file from flash memory
try:
    # creates an mp3 object
    mp3 = audiomp3.MP3Decoder(open("sound.mp3", "rb"))
    # optimizes the sample rate for your setup before playing
    mp3.sample_rate=24000
    # mp3.bits_per_sample=50
    audio.play(mp3)

    while audio.playing:
        pass

    print("Done playing!")
except Exception as e:
    print("Error in creating mp3 or playing audio:\n", str(e))

settings.toml:

# PiCockpit.com
# SSID is your network name
# replace myssid with your wi-fi name
WIFI_SSID = "myssid"

# replace mypassword with your wifi password
WIFI_PASSWORD - "mypassword"

Ten en cuenta que la descarga del .mp3 puede tardar más que la duración real del audio. Para que esto funcione, también tuve que habilitar los permisos de escritura y aumentar la memoria para la descarga mediante recolección de basura en boot.py:

# PiCockpit.com

import gc
import storage

gc.collect(150000)
storage.remount("/", False)

Añade almacenamiento externo, saca algunos botones e intenta transmitir música desde algunas emisoras de radio online. Nada te impedirá disfrutar de tu música favorita con la Raspberry Pi Pico W.

Conclusión:

El Pico W dispone de varias opciones diferentes para producir sonido, sólo asegúrate de tener cuidado con el espacio limitado que ofrece el Pico W.

Este post se basa en un artículo escrito originalmente por Zita B.

3 Comentarios

  1. Rob Frohne el diciembre 19, 2023 a las 9:23 pm

    He sido capaz de utilizar la UART para enviar fragmentos de sonido a través de USB. Puedes conseguir casi 6Mbps con el Pico USB. https://github.com/earlephilhower/arduino-pico/discussions/1765

  2. Prof Patrick Palmer el febrero 23, 2024 a las 8:07 pm

    Es muy útil. Cubres todos los métodos, en lugar de sólo un "favorito". Justo lo que necesitaba para enseñar mi clase de Electrónica Analógica Avanzada, donde utilizo el PICO en algún lugar para la mayoría de los experimentos. Tonos básicos en PWM hasta la función I2S de alta calidad. ¡Espero que otros vean mi comentario!

  3. HarloTek el marzo 9, 2024 a las 11:52 am

    ¿Y el Bluetooth?

Deja un comentario