Playing sounds from the Raspberry Pi Pico using CircuitPython – a journey of discovery

As you probably know by now, I’m not a microcontroller expert. However, with the advent of microcontrollers that use MicroPython and CircuitPython, I am a lot more comfortable using them now than I used to be! However, because the Raspberry Pi Pico is new, there aren’t a lot of practical examples of how to do things out there. There is excellent documentation, of course, but when you’re trying to do something specific, or something that hasn’t been thought of, it can be a voyage of discovery.

TL;DR

Scroll down to “Playing a WAV file” for the code that works and “Finding the Hardware” for a wiring diagram.

The project

I am currently working on a new version of my long-running Picorder project, my version of the classic Star Trek prop, the Tricorder. Whereas in the past I’ve used exclusively regular Raspberry Pi (with an occasional foray into Arduino to get analog inputs) as the brains, this time I am choosing to use the Pico. A PicoPicorder, if you will. One thing I’ve never done before is to reproduce the sounds made when the device is opened and scanning.

Finding the sound effect

My first job was to find the sound that I wanted to play. This was easy. TrekCore has a whole host of sound effects from the Star Trek series. I picked the one I wanted (this one) and downloaded it. I knew straight away that the full file was too long (and large), so I converted it, using Audacity, into a WAV file, squashed it into a mono sample and chopped most of it away, leaving me with a self-contained, much shorter, loopable sound effect.

How to play it

Now… how to play it. To start with, I looked at creating my own circuit and using PWM Audio. This post from Greg Chadwick indicated that I could do just that. However, it was, excuse me, a bit beyond me. It also used C/C++ as a programming language. I had already decided that, for this project, I would use Adafruit’s CircuitPython derivative of MicroPython. There were a lot of libraries, it is, basically, Python (which I can already use to a certain extent, thanks to the CamJam EduKits and all the other tutorials that are available), and it is extensively documented.

However, that blog post, and some previously-read Pico documentation, pointed me to needing to use I2S audio, which is how audio (DAC) boards work with the Raspberry Pi. This would give me high enough quality audio to play my sound effect and give me a bit of familiarity (which I always think is key to understanding how to do something).

The Raspberry Pi Pico and I2S

However (again), from my past reading I knew that the Pico does not have I2S pins in the same way that the regular Pi does. Some microcontrollers do have that built-in, but the Pico does not. I also knew that, in order to give functionality to the Pico that it does not have, you use PIO (Programmable Input/Output). (If you want to know about how to understand PIO even more, there’s this excellent article). It’s actually quite difficult to find “how to’s” on the Raspberry Pi website for PIO… in fact, I completely failed, so you will have to root around a bit.

First of all, I discovered this example from the Raspberry Pi GitHub repository that showed what PIO code you needed in order to accomplish that. However, because it’s written in C/C++ and is targeted at the SDK method of programming the Pico, rather than CircuitPython, it rather blew my mind. This is the important part, the PIO code:

.program audio_i2s
.side_set 2

    ; /--- LRCLK
    ; |/-- BCLK
bitloop1: ; ||
    out pins, 1 side 0b10
    jmp x-- bitloop1 side 0b11
    out pins, 1 side 0b00
    set x, 14 side 0b01

bitloop0:
    out pins, 1 side 0b00
    jmp x-- bitloop0 side 0b01
    out pins, 1 side 0b10
public entry_point:
    set x, 14 side 0b11

Complete gibberish, am I right? It was at this point that I almost gave up. 0b10? jmp? It’s assembly language, I thought, for the particular implementation on the Pico. Fair enough, but I have no idea what it all means. However, did that mean that I couldn’t use it? I had seen an example in MicroPython for using PIO inside Python (thanks Alister!) itself, but not CircuitPython.

I thought I’d step back and approach things a different way.

Finding the hardware

I weighed up the amount of problem-solving skills I had and, rather stupidly, assumed that I would be able to get I2S working, somehow.

If I could get I2S working, then I knew I would need a DAC and an amplifier to output the sound from the Pico. I came across this tutorial over on the Adafruit site for using the MAX98357 I2S mono amp which combined an I2S amp and a DAC on the same board. Ideal. It only outputs to one speaker but, for this project, one small speaker is all I need. I found the breakout on The Pi Hut’s site and they were kind enough to send it to me with some other bits for review purposes.

Looking more at the tutorial on Adafruit, I came across the CircuitPython wiring test. This shows how to hook up the MAX board to a microcontroller with I2S pins. To my dismay, the code example did not cover the PIO part. Okay, I thought, we can smash that bit and the other bit together somehow.

The MAX board arrived the next day and I wired it up. Here’s my wiring (thank you Adafruit and Fritzing for the software and parts library!):

Now that was done, PIO was the next step. Or so I thought.

Using PIO in CircuitPython and the Adafruit Discord server

By this point, I knew I was going to need some extra help. Adafruit has, amazingly, created a Discord channel for discussion of their products and, Hallelujah, CircuitPython. There was even a special channel for PIO. I joined and then realised how active the channels all were – awesome, so much expertise and experience.

From suggestions received there, I came across this tutorial on the Adafruit site for using PIO in CircuitPython, so I finally knew that what I was trying to do was possible. Somebody then pointed this example out to me that uses PIOASM (PIO assembler) to create audio on the Pico using CircuitPython.

Wanting to know how things work

At this point, I lost several hours trying to combine things together before realising that I ought to start from first principles.

Returning to the example from earlier which shows the PIOASM example to create audio in CircuitPython. All the example does is to create the necessary PIO code, assemble it, and then use it to play a sine wave.

To my surprise, once I’d got the pins sorted out, it crashed. I got the following error which others might get, so here it is:

Traceback (most recent call last):
File "code.py", line 40, in <module> File "adafruit_pioasm.py",
line 65, in assemble IndexError: list index out of range

Hmmm. Vexing, that.

I had come across this video, a “Deep Dive” with Scott, who works on CircuitPython for Adafruit:

And I wondered… was the functionality so new that I was using a too-old version of CircuitPython and its libraries? I downloaded the latest stable build of the CircuitPython UF2 from this page which was 6.2.0-beta.4 at the time. I also downloaded the latest version of the Libraries and updated the PIOASM Python file from there onto my Pico. I ran it again and… it worked! It worked! Awesome! That wasn’t the end of the adventure, though… I wanted to play a WAV file.

Playing a WAV file

I returned to the CircuitPython wiring and test page for the MAX board. This contains the following code:

import audiocore
import board
import audiobusio

wave_file = open("StreetChicken.wav", "rb")
wave = audiocore.WaveFile(wave_file)

audio = audiobusio.I2SOut(board.D1, board.D0, board.D9)

while True:
    audio.play(wave)

while audio.playing:
    pass

This simply creates an object containing a sample WAV file (StreetChicken), sets up an I2S device and then plays it until it’s finished.

I changed the D1, D0 and D9 references to my own pins and added the argument names for clarity:

audio = audiobusio.I2SOut(bit_clock=board.GP10, word_select=board.GP11, data=board.GP9)

This matches the wiring diagram above. At this point, I knew I was blundering about in the dark and I knew it wouldn’t work. No PIO stuff evident there, after all. The Pico wouldn’t know what to do with itself. Right?

Wrong.

The sample, which I had copied to the root of the Pico’s CIRCUITPY drive, came out of the speakers. It was distorted and repeated over the top of itself, but it was playing! What the heck?

Rolling with it

I decided to roll with it. I changed the code to simply play the WAV file once and then sleep. This resulted in the following (you might need to turn the volume up):

 

It was quite distorted, as you can hear, but it did match what I could play on my laptop. A piece of electronic hip-hoppy music. But why did it work?

I asked on Twitter, I asked on the Adafruit Discord channel and Peter Onion asked me whether I knew what the I2SOut code did. I did not. I received a pointer to the audiobusio/I2SOut code on the Discord channel and took a look. To my surprise, it had all the PIO code in it! That means… That means that someone else has done all the hard work with the PIOASM stuff… That’s amazing!

Clearing up the sound

I spoke to David Glaude on the Discord channel about the problem I was experiencing with the glitchy sound (see video above). He let me see his test script (which was adapted from the previous examples I noted above). I tried it out. It loops the StreetChicken music 10 times. It showed that the glitching problem was still there and also pointed to a problem with the while i2s.playing part – there might be a bug (which David has reported). David also suggested that there was an issue with the Pico being connected to the laptop – i.e. the reading of the flash memory was interfering with the I2S playback. I tried it with a wall PSU and, indeed, this cleared the issue up.

I then altered the code to play back my sound sample of the tricorder in a loop, based on David’s code. To my delight, it works perfectly (if connected to a non-reading power source). You might need to turn your audio volume up a bit, but this is what it now does!

In conclusion

You can get sound, quite easily, out of a Raspberry Pi Pico using CircuitPython. Use audiocore to open and play your WAV files over I2S with the CircuitPython audiobusio library from Adafruit doing the heavy lifting with regards to PIO. And get on the Adafruit Discord channel – there’s a lot of help available out there! 🙂

34 comments for “Playing sounds from the Raspberry Pi Pico using CircuitPython – a journey of discovery

  1. Great blog. I’ve not got into sounds yet but this will be very useful when I do.
    I totally agree with you about how helpful the Adafruit CircuitPython community are in helping people quickly solve a problem.
    I fear that MicroPython on the Pico will be left behind because of the wide availability of sensor libraries with CircuitPython and the total lack of progress so far in producing any for MP.
    MicoPython will be left to those who need to use both cores and interrupts.

  2. Great post! I’m looking to experiment with sound on the Pico and this was timely.

    One question – I’m confused by your use of the phrase “(if connected to a non-reading power source)”.

    I know what most of those words mean, just not in that order… 😉

  3. Hi, Michael,

    Great post! There is only one thing I do not understand yet: where sits the .wav file? In the 2Mb external flash on the Pico board? Adafruit has some great instructions to generate .WAV files, but I cannot find where and how to store it in the Pico memory. Any idea?

    • You can put it “anywhere” as long as you tell it where to find it. But in my case, I’ve just put it right at the top of the CIRCUITPY drive. In my case, that’s E:\ but depends what you’re using.

      • Hi Michael,

        Thanks for your answer. Is E:\ some location on a PC harddisk or is it the Pico Pi popping up as an USB drive?
        CircuitPy is an interpreter, but it looks if the program in CircuitPython is compiled, together with the WAV file and transferred to the Pico Pi 2Mb flash memory which is E:\ ?
        I am not yet familiar with CircuitPy nor Pico Pi, but I try to find a microcontroller for my audio application: a simple sound playback unit, just running in a cheap, single micro controller (and audio amp of course). In your program you open the WAV file as binary read, but I do not see any path to the file, or in your words “as long as you tell it where to find it”. I am still confused.

        Thx, Guus

        • When you plug in your CircuitPython device (having first of all loaded the Firmware onto the device), it appears as CIRCUITPY. On Windows, this is a drive letter. In my case, when I plug it in, it comes up as E:\
          The CircuitPython code.py file isn’t compiled – you just drop it on the drive. Same with the .wav file – you drop it onto the drive.

  4. Michael, thank you very much for the explanation. My plan is to buy some picos but they all are sold out. Brexit or Suez?

    Kind regards, Guus

  5. Hello Micheal,
    Perfect and thank you – this was just what I was looking for! I have one question that you may be able to help me with. My application (which involves the Pico and playing of WAV files) will require interrupts which Circuitpython doesn’t yet support. I wondered if CircuitPython libraries work with MicroPython?
    Thanks again
    Kevin

    • They won’t “just work” in MP but you might be able to find libraries that will work just as well, or almost as well. You might also be able to convert them. Interrupts are also giving me pause about continuing to use CP…

  6. The lack of interrupts in CP is a niggle indeed.

    I really can’t make my mind up between MP and CP. Currently, I’m slightly swayed towards CP because of the library support. I think I can get around CP interrupts and the lack thereof with fudge and imagination!

  7. Hi, I tried to rebuild your setup, but for some reason no sound comes from my speaker. The code itself executes successfully. Except for the speaker, my setup is identical to yours. I also tried the same setup on the raspberry pi and there it seems to work. Do you know what could be the problem?

  8. Hi Micheal,

    At last I all pieces of hardware came in. An RP Pico, an Adafruit MAX98357A module, a breadboard and M2M wiring cables.
    I started connecting everyting, exactly following your post.
    First I started everything on my Linux Debian Buster PC. I downloaded the circuitpython .UF2 file (version 6.2.0, latest version) for the RP Pico and did copy it on the RP, which correctly turned into a working CIRCUITPY USB drive. I used the Thonny IDE, version 3.3.6 and started to write some simple blinking LED programs. That all works fine. After that, I took the code from your blog (playing a wave file), downloaded the “StreetChicken.wav” file into the Pico, corrected the pins to GP10, GP11 and GP9 and started it. Nothing…. No sound, nothing, perfectly soundless. Thonny gives no errors and it seems it runs on the Pico. So I started troubleshooting: checked the wiring again and again, used my voltmeter and saw the 3.3V coming in on the MAX unit. I tested GP9, 10 and 11 by writing a LED output to it, which worked out perfectly (no defective outputs). Since I ordered 2 MAX98357A units, I did replace them, as I did with the loudspeaker (several tries, 4Ohms, 8 Ohms). But no sound. I added some print statements in the code, which are printed well in the REPL. Nothing made the stuff work.
    at last, I tried to switch OS. I took a Windows 10 PC, installed Thonny 3.3.6 and tried everything again. No sound! But something else: Thonny comes up with an error message: ModuleNotFoundError: No module named ‘audiocore’. Strange, because the import audiocore statement is cleary available in the code. It is its first line.

    Do you have any idea what happens here? The last post of Joe reports a similar problem, but his answer ends in some loudspeaker type, not in a further solution.

    Thank you,

    Guus

    • Hi all,

      The reason why my pico plays no I2S sound is odd. Since I am Dutch, the Circuitpython.UF2 file from Adafruit is presented in Dutch. Using this file on the Pico, it plays no “streetchicken.wav”, but the sound of silence only. I observed this when troubleshooting my application with my oscilloscope on the BCLK, LRCLK and DATA outputs of the pico. Because Micheal has no issues like that and he is from the UK, I began to understand it. I cleaned up the flash with flash_nuke.uf2 and reinstalled the Adafruit circuitpyton en-GB 6.2.0 UF2 file. After that, my application played “streetchicken.wav” like in Micheals post.
      I was not aware, Brexit digs so deep…..

      Guus

  9. Hello Michael, thank you for this blog! However, I’m having a little trouble with the basic principles and I’m not certain if it’s my hardware setup or I’m just missing some software steps… so I followed the basic pins into a breadboard the only difference is I’m using a terminal to female 3.5mm jack (https://thepihut.com/products/3-5mm-1-8-stereo-audio-jack-terminal-block) to externally powered pc speakers from the amp. I’ve used the circuit python code from adafruit for wav output, altered the line for pinout (GP) although an error occurred stating line was too long, so I removed the tags back to board=GP… all I get is a static/white noise sound if I turn up the speakers to near full volume. I’ve tried placing the + pin into L and R but no change… In completion I’m hoping to add a PIR sensor and two red LED lights (https://thepihut.com/products/diffused-red-and-green-indicator-led-18mm-round) along with the sound to play for a Halloween project (my profile pic from which you originally contacted me)… I though I had successfully ‘mashed’ the codes together, start with the dependencies, then the setup and pins, then the action code… but like I said, nothing. So I went back to basics with just the audio code and hardware, still nothing. Sorry for the long post, hope I was descriptive enough. Thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.