Cheap stereo line out I2S DAC for CircuitPython / Arduino synths

I am a big fan of these PCM5102A I2S DAC boards (3 for $15! affilliate) for playing around with audio synthesis, for example in this video about new `synthio` CircuitPython library or this repository about Mozzi Arduino synthesis. They have pretty high-quality audio output up to at least 16-bit @ 44.1kHz and separate out all the audio circuitry onto its own board with its own regulator! This makes it much less likely that your circuit’s noise will infect your audio. And the line out is strong enough to drive most headphones. The board itself is pretty small too at 29mm x 16mm.

Hooking the up PCM5102 board is pretty easy for the general case:

  • SCK – Gnd
  • BCK – I2S bit clock pin
  • DIN – I2S data pin
  • LCK – I2S word select pin
  • GND – Gnd
  • VIN – 3.3V

In the case of hooking up the PCM5102 board to an Adafruit QT Py RP2040, one way to do that is like this:

The corresponding CircuitPython setup using audiobusio.I2SOut() for the above wiring looks like:

import board, audiobusio
i2s_bck_pin = board.MOSI # PCM5102 BCK pin
i2s_lck_pin = board.MISO # PCM5102 LCK pin
i2s_dat_pin = board.SCK  # PCM5102 DIN pin
audio = audiobusio.I2SOut(bit_clock=i2s_bck_pin, 
                          word_select=i2s_lck_pin, 
                          data=i2s_dat_pin)

Note that the back of the PCM5102 board has several solder jumpers to configure the chip. Most vendors will set these to a good default value, but verify if they match the above inset photo above or the yellow box in the photo below.

Most of the information I gleaned about the PCM5102 board I got from this macsbug blog post. It is very handy. From it is also the schematic for the PCM5102 board, included below:

Here’s a complete SD-card based WAV player using a Raspberry Pi Pico and a PCM5102

And the CircuitPython code that plays WAVs off an SD card:

# i2s_sdcard_pico.py -- I2S Audio from SD Card on RP2040 Pico
# 20 May 2022 - @todbot / Tod Kurt

import time
import board, busio
import audiocore, audiomixer, audiobusio
import sdcardio, storage, os

# pin definitions
i2s_bclk = board.GP9  # BCK on PCM5102 (connect PCM5102 SCK pin to Gnd)
i2s_wsel = board.GP10 # LCK on PCM5102
i2s_data = board.GP11 # DIN on PCM5102
sd_mosi = board.GP19
sd_sck = board.GP18
sd_miso = board.GP16
sd_cs = board.GP17

# sd card setup
sd_spi = busio.SPI(clock=sd_sck, MOSI=sd_mosi, MISO=sd_miso)
sdcard = sdcardio.SDCard(sd_spi, sd_cs)
vfs = storage.VfsFat(sdcard)
storage.mount(vfs, "/sd")

# audio setup
audio = audiobusio. I2SOut(bit_clock=i2s_bclk, word_select=i2s_wsel, data=i2s_data)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
                         bits_per_sample=16, samples_signed=True)
audio.play(mixer) # attach mixer to audio playback

# find all WAV files on SD card
wav_fnames =[]
for filename in os.listdir('/sd'):
    if filename.lower().endswith('.wav') and not filename.startswith('.'):
        wav_fnames.append("/sd/"+filename)
wav_fnames.sort()  # sort alphanumerically for mixtape numbered order

print("found WAVs:")
for fname in wav_fnames:
    print("  ", fname)

while True:
    # play WAV file one after the other
    for fname in wav_fnames:
        print("playing WAV", fname)
        wave = audiocore.WaveFile(open(fname, "rb"))
        mixer.voice[0].play(wave, loop=False )
        time.sleep(3)  # let WAV play a bit
        mixer.voice[0].stop()

Speed up CircuitPython LED animations 10x!

In many LED animations, you need to apply an operation to all LEDs as fast as possible. Often this is something like “fadeToBlackBy”, i.e. “dim all LEDs by a given amount”. There are some common effects you get by using “fadeToBlackBy” and one other action. For example: the Cylon effect is “Turn an LED on, Fade all LEDs toward black, Go to next LED, Repeat”. A simple firework simulation is “Turn on random LED, Fade all LEDs toward black, Repeat”.

Since I don’t come from a normal Python background, I’ve not used much Numpy. But I knew it was great for vector math. And I knew CircuitPython had a minimal version of Numpy called ulab it inherited from Micropython. Even though I’d seen the wonderful ulab learn guide that jepler did, the usefulness of numpy/ulab didn’t sink in. It was only when I was hacking on LED animation speedups did I re-stumble upon ulab. And am I glad I did.

As a test with a simple fire animation (as optimized as I could make it in normal Python), the results with ulab are striking:

The general technique is to create a “working copy” of the LED data in a ulab array, and use ulab functions as much as possible to modify that working copy. Then at the last possible moment, copy the working copy data to the real LEDs object. It only adds a few lines of code to existing solutions and you get access to all these cool ulab array functions for LED effects! For instance, to constrain all RGB values of all LEDs to 0-255, this is a single line in ulab: leds_np = np.clip(leds_np,0,255). And it further cements my belief that any time you’re doing a for-loop on a large list in Python, you’re probably doing it wrong. :-)

Screenshots of functionally identical code, before / after:

The video demo below shows the difference (also available on youtube). Each setup is identical, calculating for 256 LEDs even though only 64 are displayed. The “no ulab” case is perfectly usable if a bit choppy, but you lose ~36 milliseconds where you could be doing something else. (Like, for instance, these LED matrices are wired up in a serpentine pattern, so you need some math to unravel that if you want to draw shapes). The ulab version seems much smoother and more organic, at least to my eyes.

If you’d like to try this yourself, I’ve collected this code and a rudimentary “fire_leds” library that uses this technique in the repository: https://github.com/todbot/circuitpython_led_effects

CircuitPython in 2022

CircuitPython is an amazing microcontroller programming platform, running on many different chips and boards while providing a consistent API. I’m pretty experienced with embedded development and Arduino, but have little Python knowledge. So for the last few years I’ve been using CircuitPython to teach myself Python. The results I’ve been logging via my QTPy Tricks and CircuitPython Tricks repos. CircuitPython is one of the most fun and useful tools in my toolkit now.

At the beginning of every year, the community reflects on CircuitPython and their plans for its future. This year the tag is “#CircuitPython2022“. Here are my (belated) plans / desires. My overall theme is: audio stuff!

The Big Ones

These are the large efforts that are year-long in scope and perhaps not even possible. But any would put CircuitPython on an entirely new level of usefulness for me.

  • Modular audio engine. The Teensy Audio Library (TAL) is hugely powerful, able to create synthesizers, audio effects, just about anything to do with real-time audio in and out of a microcontroller. It would be amazing to port it to CircuitPython. CirPy already has pretty robust support for DMA-based audio streams, a main stumbling block to get this functionality I think, but I really don’t know if it’s feasible. There does seem to be the beginnings with audiomixer.Mixer and synthio, but I know nothing. From my recent playing with the Mozzi synth library and Teensy Audio Library on SAMD51, I am eager to see if those concepts can be applied to CircuitPython. I expect it’s beyond my abilities but it would be very cool to see CircuitPython being used to describe audio flows to create custom audio devices. See circuitpython#4467 for more
  • Periodic code block invocation. So much synth & game code needs regular timing. Example: how would you create a metronome in CircuitPython? Currently one does this by hand by checking time.monotonic() or supervisor.ticks_ms(). This is fine but if you have a displayio display or adafruit_seesaw devices, you are at the mercy of the various delays inherent to those libraries. Yes, this is a variant of the “why no interrupts?” argument, but if it’s periodic and under the control of the CircuitPython supervisor, perhaps the issues can be dealt with?
  • USB Host! I’m very excited by some recent additions in TinyUSB that adds a USB MIDI host driver. But how cool would it be to plug in a USB keyboard, MIDI controller, or thumbdrive to a CircuitPython device and have it work? See circuitpython #5630 for more

“Small” Changes

These are issues I’ve experienced or changes I want but haven’t characterized enough to submit an issue. And they’re small enough that maybe I can actually contribute on.

  • Allow play start/end points & loop start/end points for audiocore.WaveFile and audiocore.RawSample. Currently you can tell a sample to loop and it works! This is awesome. The looping functionality could be so much more powerful if the start/end points could be modified.
  • Allow RawSample buffer to be replaced. Currently once you create a RawSample, it’s read-only.
  • Why does audiocore.AudioMixer seem to mess up with multiple small RawSamples?
  • The adafruit_seesaw library and its descendants are very conservative about timing with explicit time.sleep() calls to ensure good data. This kills update rate when using several Seesaw devices. I’d like to try tuning these or at least propagating up the delays to the user.
  • Newbie-friendly documentation changes for “Core” modules. The docs for the built-in C-based libraries are really good if you know what you’re doing. They function well as reference material. But if you’re new to a concept, they can be a bit mystifying. And you can’t look at the Python source because they’re in C. I’ve had this problem a few times and this point is a reminder to myself: when this happens to me again, make a PR with what I think would help.

Projects for 2022

Some projects I plan on doing in CircuitPython in the coming months:

  • Tiny USB + Serial MIDI keyboard using KB2040 and Kalih key switches. Oh no another CircuitPython keyboard! I always need small MIDI controllers. The OMX-27 I have is great, but I broke its encoder, it’s hard to repair, and I want something smaller. And I’ve not played in the “keeb” space so this’ll be my entrée to that world.
  • “Additive synthesis” using stacked RawSamples. In CircuitPython it’s really easy to compute and play samples on the fly. Can I use this to create a kind of wavetable synthesizer by mixing samples through an audioio.AudioMixer?
  • Seesaw Knob & Button board. Of the little Seesaw IO expander board’s 15 GPIO pins, 9 can be ADC pins! I want a Seesaw board that has 8 pots, 4 buttons and some NeoPixel / WS2812 LEDs. This seems like it should be a thing? Why isn’t this a thing? I will make it a thing.
  • “Tiny LED Effects” library submitted to the CircuitPython_Community_Bundle. This is a small collection of functions I use on QTPy M0 where adafruit_led_animation and adafruit_fancyled will not fit. It’s a neat bunch of generative LED effects with its own optional cutdown version of neopixel, I just need to package it up.

Closing Thoughts

The CircuitPython community is the best I’ve experienced. I feel privileged to be considered part of it. I’m always learning something new about some aspect of Python, different processor architectures, or some new algorithm or technique. If you’re interested in CircuitPython, please join the Discord! I can’t wait to see what 2022 brings.

More Mozzi Experiments

Over on github, I’ve been extending my experiments with the Arduino Mozzi synth library. Mostly I’ve been focussing on SAMD21 chips (QT Py M0, Trinket M0) and the RP2040 chip (QT Py RP2040, KB2040, etc) because SAMD21 support is in the official Mozzi repo and there’s a RP2040 Mozzi port.

The SAMD21 has a built-in DAC so the output circuitry can be pretty minimal. The RP2040 port is PWM like on the Arduino Uno (ATmega328).

Sketches

  • eighties_dystopia – A swirling ominous wub that evolves over time (there’s also an RP2040 version)
  • eighties_dystopia_rp2040 – Same thing, but on a Raspberry Pi Pico
  • eighties_arp – An arpeggio explorer for non-musicians and test bed for my “Arpy” arpeggio library
  • derpnote2 – A sound like THX “Deep Note” with 16 saw oscillators that converge to a chord

Arpy Arpeggiator Arduino library

Inside of the “eighties_arp” directory is “Arpy.h“. This is a small arpeggiator class that has some knowledge about music theory. An example that uses this is John Park’s Arcade Synth Controller. The library’s API is purposely very simple:

void loop() {
  // read knobs, set root_note and bpm
  arp.setRootNote( root_note );
  arp.setBPM( bpm );
  arp.update();
}

Some Demos

Mozzi Arduino synth lib on Oskitone Scout

Mozzi is an audio synthesis library for Arduino that can do multi-oscillator synthesis with filters and modulation on even an Arduino Uno.

Oskitone Scout is an adorable tiny keyboard kit, based on an Arduino Uno and entirely open source.

I’ve been playing with Mozzi recently after first hearing about it many years ago. The Scout seems like a perfect platform for those experiments. I’ve been putting on these experiments in the MozziScout github repo.

Here’s some examples of using Mozzi on Scout, along with the small mod needed to make it work.

  • mozziscout – a straight-forward Mozzi version of the ‘scout‘ sketch that comes on Scout
  • mozziscout_monosynth1 – a fat mono synth, made of two detuned oscillators and a single resonant low-pass filter. Also features different startup modes to tune synth paramters. (see sketch for details)
  • mozziscout_drums_bass – a 4-voice drum sample kit (bd,sd,oh,ch) and a single oscillator synth in one!
  • mozziscout_chordorgan – a 5-oscillator synth with built-in chords! Sort of like Music Thing Modular’s Chord Organ.
  • mozziscout_poly – a poly synth, sort of. Scout’s keyboards doesn’t allow true playing of chords, but with a slow release envelope and the playing of arpeggios, you can make chords
  • mozziscout_wash – a five-oscillator stacked chord sound based on the Mozzi example Control_Oscil_Wash
  • mozziscout_thx – an eight-oscillator stacked sound where the oscillators start at a random pitch and slowly converge to a chord. Vagugely based on THX Deep Note

Demos

“mozziscout_monosynth1”

“mozziscout_poly”

“mozziscout_thx”

How to mod your Scout

One way two swap this is where the two legs of the ATmega328 chip are lifted, and jumper wires are soldered on and plugged into the socket, as in the photo below.

Using normal Scout code

After this mod, you can still use your Scout like normal, just make sure to swap pins 9 & 11 in the code.

const int SPEAKER_PIN = 9;  // was 11
byte rowPins[ROWS] = {7, 8, 11, 10};  // was {7, 8, 9, 10}

How to install Mozzi

Mozzi is not in the Arduino Library Manager, but installing it is pretty easy anyway. The installation process is the same way you hand-install any Arduino library.

  • Go to https://github.com/sensorium/Mozzi and click on the green “Code” button, then click “Download ZIP”.
  • You will get a “Mozzi-master.zip” file. Rename it to “Mozzi.zip”
  • In the Arduino IDE, go to “Sketch” -> “Include Library” -> “Install .ZIP library…”, then pick your “Mozzi.zip” file.
  • Once it completes, you will see Mozzi in the list of available libraries and the Mozzi examples in “File” -> “Examples” -> “Mozzi”. Many of the examples will work with Scout but not use the keyboard.

Using Mozzi

  • Mozzi is very particular about what is in loop(). Do not put anything else in there. Instead put it in the void updateControl() function. See the sketches for examples.
  • Mozzi output is quieter than standard Scout (which outputs full-width square waves). Use an external amp for best results.


Two-Wire NeoPixel WS2812 LEDs

Neopixel LEDs with only two wires?! Just data & ground? What kind of magic is this?

This is my take on this hack that Parallax Propeller forum user “Tubular” posted about in 2014 (!)

The trick is that the data line is carrying power, and a capacitor and Schottky diode are on the strip to separate out the data from the power. The capacitor holds the charge (mostly) for the LEDs as the WS2812 data line goes LOW and the diode keeps the capacitor’s voltage from interfering with the data line.

To keep the strip powered, make sure to set the data line HIGH after every frame of data sent. This keeps the strip powered. This technique works with most any WS2812 / NeoPixel library on Arduino or CircuitPython. If you want to try two-wire NeoPixels out in Arduino, one way is to change your “strip.show()” (if using Adafruit_NeoPixel library) to the function “strip_show()” shown below. The example a lightly-edited version of Adafruit_NeoPixel’s “strandtest_wheel”.

For CircuitPython, check out this gist.

QTPy-knob: Simple USB knob w/ CircuitPython

I like minimal solutions to problems. I was playing with a CircuitPython-enabled QT Py on a breadboard with and a rotary encoder and I ended up making a USB knob, like many others have done before. But I realized: waitaminute, I can literally just plug the encoder directly onto the QT Py…

Thus was born the QTPy-knob. It’s one of the simplest USB knobs I’ve come across and it’s because the happenstance that a rotary encoder can usefully be plugged directly into a QT Py board and that CircuitPython is so powerful now it’s just a few lines to go from rotary encoder pulses to sending arbitrary USB keyboard or mouse commands.

I really liked the Griffin PowerMate from over a decade ago, so I decided to design a 3d-printable enclosure that echos the design of the PowerMate, but that works with the restrictions of the QTPy+encoder stackup and a Neopixel ring. The result is pretty good I think.

All design files are in the qtpy-knob github repo.

The code is must easier now than the media-dial project I started cribbing from. This is due to the fact that CircuitPython now has native support for rotary encoders and this knob is trying to do much less than media-dial project.

The electrical wiring is virtually non-existent, only needing three wires if you elect to add a Neopixel ring.

The assembly is described in the video above a bit, but you can also see what’s up in this CAD animation (thanks to the Adafruit_CAD_Parts repo for making the enclosure design easier)

The result with opaque plastic and clear LED diffuser ends up looking pretty cool.