Close

Polyphonic sound using I2C!

A project log for MakerNet

The fast, intuitive and modular way to make awesome portable projects that maximize creativity and sharing

Jeremy GilbertJeremy Gilbert 06/20/2017 at 17:110 Comments

I'm a big fan of devices that make sounds as a way of interacting with the user. Pretty much every app, game and operating system uses sound in some way and it feels very natural.

Sadly, adding high quality sound into a hobby-grade EE project is kind of a pain in the bottom. The Arduin supports a Tone library, but this is quite limited. You only get one sound playing at a time, and its basically a square wave. Adafruit and others have come up with WAV shields that fill in the gaps. With these neat devices, you can actually take sound files from your PC and set them up to be triggered by your project. The best winner in this category has to be the prop shield and audio library from PJRC.

Even with these new devices, there is still a lot of complexity. And you don't really get everything you'd want.

For instance:

Based on these experiences, I was determined to integrate a true polyphonic sound player into the Makernet arsenal. Behold, the result of my efforts:

As you can see, this little baby is still being prototyped (note the dangling USB cable and programing jig). It has a MicroSD card reader, a SAMD21G18 MCU and plays sound out of a MAX98357A I2S audio chip. Of course it also has the regular 6 pin makernet header that provides it with network, 3.3V and VIN (usually 5V).

Using this device inside Makernet code is really easy.

First you declare an SoundPeripheral object like this:

SoundPeripheral soundPeripheral;
The Makernet framework will connect your sound peripheral hardware to this object. Now when you want to play sounds, you can simply call:
soundPeripheral.triggerSound(1)

By way of a demo, I made a very simple setup with a Makernet mainboard (a SAMD21G/M0), a DPad button and the Sound Board.

The following code snippet is all you need to have a sound play on a keypress. The Makernet framework handles all of the network discovery, message passing, error handling, etc.

#include <Makernet.h>
#include <Wire.h>

SoundPeripheral soundPeripheral;
DPadPeripheral dPadPeripheral;

void setup() {
  Serial.begin(57600);
  Makernet.begin();
  dPadPeripheral.onButtonPress( buttonHandlerPress );
}

void buttonHandlerPress( DPadPeripheral *p, uint8_t channel )
{
  soundPeripheral.triggerSound(55);
}

void loop() {
 Makernet.loop();
}

You can see a quick demo here:

Writing the code for the Sound Peripheral involved a lot of learning. For the uninitiated, real time sound programming is a bit of an adventure.

First I had to get the I2S library working properly. The library supports DMA, and you hand it a buffer of things to play periodically.

Next, I needed to add double buffering, so that sound can be generated in batches and then handed off to the DMA. Otherwise, new sound data generated by the audio sources would trample over old sound data.

Next, I needed a full audio framework. I started with the PJRC/Teensy libraries that I admire so much. They are brilliantly written, but assume Cortex-M4 DSP instructions which I don't have on the Cortex-M0. Secondly, Paul makes liberal use of a very sophisticated hand-tuned memory library presumably so that each audio source can avoid unnecessary memcopy() calls.

I ultimately decided it would simply be easiest to implemeny my own library using some of the design patterns I like from the PJRC stuff. In my design, there are a set of interconnected objects of type AudioStream:

class AudioStream {
  public:
    AudioStream() {};
    uint8_t streamOutput[AUDIO_BLOCKSIZE];
    virtual void reset();
    virtual void update();
};
I then subclassed for every audio source I needed from waveform generation, SD card playback and of course sound files written in headers. I wrote a simple Perl script that opens up an audio file and converts it into a buffer like this:
// Converted by raw2code.PL, inspired by the Teensy/PJRC wav2sketch
// Samples are assumed to be unsigned uint8_t from input file, 22050 Hz mono 8-bit
const unsigned char magic_aiff[111726] = {
0x80 ,0x7F ,0x80 ,0x7F ,0x80 ,0x7F ,0x80 ,0x7F ,0x80 ,0x80 
,0x80 ,0x80 ,0x80 ,0x80 ,0x80 ,0x7F ,0x80 ,0x7F ,0x80 ,0x7F 
I also implemented a mixer that averages sound sources together.

Like most of my Makernet activities, this is largely in the proof of concept stage. I have it working. But its currently quite fragile and needs many more features implemented to be a proper product.

Discussions