Pi Pico PIO Poly Tone Keyboard

One of the obvious use-cases for the Raspberry Pi Pico’s Peripheral I/O system (PIO) is in generating simple tones.  Ben Everard from Hackspace Magazine did exactly that.  You can read the full tutorial here or in Hackspace Magazine Issue 40.

All the code for his PIOBeep.py can be found on GitHub here, and he leaves us with the note for an extension – “Can also have up to 8 of these running to make a polyphonic sound. Possibly based on button input”…

So cue my Arduino Tone Polyphony and my Pi Pico MIDI Matrix Decode where I combine some of the ideas to give my Pi Pico a simple voice of its own!

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

These are the key Arduino tutorials for the main concepts used in this project:

If you are new to microcontrollers, see the Getting Started pages.

Parts list

The Circuit

PiPicoPolyToneKeyboard_bb

The blue wires go off to the 12 buttons, one per note in an octave, and the green, yellow, orange and purple wires go off to the button’s common pins, one per octave.  Full details of this part can be found here (and in subsequent posts as required).

I am using the Pico’s IO pins 14 to 21 as the square wave tone outputs, each of which will be driven by the PIO system.  They are connected via a 1kΩ resistor to make a simple passive mixer.  Then the output is passed through a 10uF non-polar capacitor to remove any DC bias, making the signal largely suitable for an audio line input (cue usual warning here about not using anything expensive by way of amplification in case something goes wrong).  Full details of this part of the circuit can be found in my previous Arduino tutorials here.

2021-02-19 14.58.45

The Code

Most of the hard work has already been done by Ben in his PIOBeep.py library module.  You can download it here, by downloading all the code in the repository (the green Code button), opening the ZIP file, copying out the PIOBeep.py file somewhere and then saving it to your Raspberry Pi Pico.  It can then be used directly using a simple “import” statement.

There are two modifications I made for this example though.  First there was a bug in the initialisation of the PIOBeep class.  The first parameter to the StateMachine call should be “sm_id” not “0” as shown below:

 self.square_sm = StateMachine(sm_id, square_prog, freq=freq, sideset_base=Pin(pin))

Then there was a small enhancement I needed.  The original library will play a note with a specified duration, but I need it to start a note and leave it playing until I tell it to stop.  So I added a note_on and note_off function as follows:

class PIOBeep:
def note_on(self, val):
self.square_sm.active(1)
self.square_sm.put(val)

def note_off(self):
self.square_sm.active(0)

A good chunk of the code is the same as used with the Pi Pico MIDI Matrix Decode – Part 3 project, so I won’t repeat the discussion here.  The difference is that the PIOBeep library has to be initialised eight times (once for each pin to be used as an output) and then the noteOn and noteOff functions need to start or stop the tones (“oscillators”) as required.  This is using the same principles as discussed in the Arduino Tone Polyphony series of projects, but this time written in Python.

The initialisation goes as follows

# Initialise the oscillators...
osc = []
oscuse = []
osc_pins = [14,15,16,17,18,19,20,21]
numosc = len(osc_pins)
for o in range(0, numosc):
osc.append(PIOBeep.PIOBeep(o,osc_pins[o]))
oscuse.append(0)

One quirk is that the PIOBeep library works in terms of special values calibrated to how the library drives the PIO state-machines.  This means that there are two interfaces to the library:

  • Using the bespoke “values” corresponding to the pitches required to be played.
  • Using the frequency of the pitch required.

If we use frequencies, then they need to be turned into the bespoke internal values each time, so it makes sense to pre-calculate then.  As I’m using code that is “MIDI aware” though, all my buttons are generating MIDI note numbers, so I actually need a way to translate between MIDI note numbers to pitch frequencies to PIOBeep magic numbers.

This happens on initialisation as follows:

midi2osc = []

for c in range(0, numcols):
for r in range(0, numrows):
midi2osc.append(osc[0].calc_pitch(firstfreq*2**((c*numrows+r)/12)))

This slots into the pre-existing code that is setting up the button rows and columns.  firstfreq is the frequency of the first MIDI note to be recognised and calc_pitch is a function provided by the PIOBeep library to generate the magic numbers from frequencies.  The “note number” is given by (c*numrows+r) and is turned into a frequency according to the formula given on the website http://newt.phys.unsw.edu.au/jw/notes.html

As with previous approaches to polyphony, I’ve written the code to keep playing until all tones are in use and then ignore new keypresses.  Another approach would be to stop the longest held note in favour of new notes, but that is more complex to implement.

Find it on GitHub here.

Closing Thoughts

I wanted to start with something self-contained and as you can see from the video, I seem to have created the worlds most impractical four-octave keyboard!  But it does work.

The next step is probably to create a MIDI version to cut down on the wires, and allow it to be played from something a little more practical.

Then it is probably time to explore other options, such as using the PIO for PWM output for some direct digital synthesis.  It is probably also time to really take a look at my Pimoroni Audio Pack.

Kevin

Leave a comment