“Bare Metal” Raspberry Pi Synth – Part 2

This post builds on part 1 and starts to add some IO handling code and simple synthesis code to the bare metal Raspberry Pi experiments.

Full index of posts in this series:

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 tutorials for the main concepts used in this project:

If you are new to microcontrollers and single board computers, see the Getting Started pages.

Parts list

  • Raspberry Pi v1 (in my case).
  • 128Mb or greater SDHC SD Card.
  • Linux installation (I use Ubuntu in a virtual machine on my Windows PC).
  • Optional: Adafruit T-Cobbler (or similar) for the RPi version you are using.
  • MCP3008 SPI 8-way analog to digital converter.
  • several 10k potentiometers.
  • Optional HDMI monitor (for testing).
  • Amplification and lead for the headphone socket on the Pi.
  • Solderless breadboard and jumper wire.

Hardware

Raspberry Pi MCP3008_bb

There are four signal connections plus power and ground required from the Raspberry Pi GPIO connector to the MCP3008, so this could be patched straight over to the breadboard, but I used my Adafruit T-Cobbler to make things easier.

The circuit above shows two potentiometers connected to the MCP3008. In the simple synth demo below, I’m using four.  The circuit will support up to eight.

IMG_5517

The Circle Environment

Details of how to install and build the default Circle environment and get it installed onto an SD card for use with your Raspberry Pi were provided in part 1, so I’ll not go over them again here.  Recall I’m using an Ubuntu virtual Linux machine for my experiments.

To start building our own code we need our own project area, so I’ve created a new src directory under the main circle area as follows:

:~$ cd src/circle
:~/src/circle$ mkdir src
:~/src/circle$ cd src
:~/src/cirlce/src$

I need a simple Makefile in here to list all the source files I’m going to be using.  For example, to build a project test.cpp with an include file of test.h, I need a Makefile that looks like the following:

CIRCLEHOME = ../..

OBJS = main.o kernel.o test.o

LIBS = $(CIRCLEHOME)/lib/usb/libusb.a \
    $(CIRCLEHOME)/lib/input/libinput.a \
    $(CIRCLEHOME)/lib/fs/libfs.a \
    $(CIRCLEHOME)/addon/sensor/libsensor.a \
    $(CIRCLEHOME)/lib/libcircle.a

include $(CIRCLEHOME)/Rules.mk

-include $(DEPS)

And then create the main.cpp and kernel.cpp files to call the functions in my test.cpp as appropriate. I based my kernel and main on those from the miniorgan project I used last time.

To make developing and building easier, I’m using the Notepadqq editor that you can find on Ubuntu.

Once the code is ready then a simple “make” in my source area will create a kernel.img file I can copy to the SD card to boot from.

MCP3008 Analog Inputs

The Raspberry Pi has no built-in analog input ports, so an external ADC is required.  A common solution is to use the MCP3008 which is connected to the Pi using SPI (you can read about it here).

By default, the Circle environment does not have support for the MCP3008, but it does exist as an add in the code.  There are two files required: mcp300x.cpp and mcp300x.h which can be found in the addon/sensor/ area.  To build, you need to go into addon/sensor and hit make.  This will create a sensor library libsensor.a which can then be included in our later build.

IMG_5515

Simple Oscillator RPi Synth

I’m starting relatively simply with just two oscillators, supporting four waveforms, that can be combined in a few simple ways.  I was initially going to write some code based on my experiments with Mozzi on Arduino, but after a while discovered a couple of existing “bare metal” Raspberry Pi synths, one of which, MiniSynth, is from Rene Stange, the author of the Circle environment.  From the readme:

MiniSynth Pi is a polyphonic virtual analog audio synthesizer, running bare metal (without separate operating system) on the Raspberry Pi. On the Raspberry Pi 2, 3 and 4 it allows to play up to 24 polyphonic voices at a time, on the Raspberry Pi 1 only 4 voices.

This is a very comprehensive software synthesizer but it does not appear to support any direct IO – it is all screen based.  So whilst I am very interested in having a look at some point, for now, I want something I can plug some simple IO into.  But I did decide to take Rene’s implementation on oscillator as the basis for my simple experiments, so I pinched oscillator.cpp and oscillator.h from here: https://github.com/rsta2/minisynth/tree/master/src and used these as my starting point.

My application started with the miniorgan sample – I’ve renamed the files and classes from miniorgan and CMiniOrgan to circlesynth and CCircleSynth then started changing things.

I’m not going to go into the code in detail (it is all on GitHub) as to be honest, I’m not convinced this is a useful place to stop yet, but I’m writing up where I’ve got to so far.  But here is the top-level structure of what is going on.

CircleSynth

  • Main.cpp – “boiler plate” start-up code – based on the miniorgan example.
  • Kernel.cpp/h – Mostly the same as miniorgan, but with additional SPI/potentiometer handling (see below).
  • Circlesynth.cpp/h – Starting from the miniorgan sample, and keeping all the code associated with the keyboard and MIDI handling, the main updates are in the GetChunk method (called from the audio handling of the Circle environment), which calculates the samples for outputting to the audio subsystem.
  • Mcp300x.cpp/h – As mentioned above, this is the driver code for the MCP3008 taken from the addon/sensor part of Circle.
  • Oscillator.cpp/h – As mentioned above, this is the implementation of an oscillator from the MiniSynth project with a few tweaks.

I’ve needed to do an update to make it all work:

  • I’ve removed any knowledge of CSynthModule from Oscillator.cpp/h to make it a stand-alone file.  I’ve also added a SetSampleRate method so that can be passed in from the main circle environment.

The analog IO handling is in kernel.cpp as follows:

TShutdownMode CKernel::Run (void)
{
   m_CircleSynth.Start();
   for (unsigned nCount = 0 ; m_CircleSynth.IsActive(); nCount++)
   {
      boolean bUpdated = m_USBHCI.UpdatePlugAndPlay();

      // New code for the potentiometers
      for (int potCh=0; potCh<SYNTH_POTS; potCh++)
      {
         unsigned nResult = m_MCP300X.DoSingleEndedConversionRaw (potCh);
         if (nResult >= 0)
         {
            m_CircleSynth.SetPot (potCh, nResult);
         }
      }
      // End of new code for the potentiometers
   }
   return ShutdownHalt;
}

Then the SetPot function in Circlesynth.cpp turns the potentiometer values into something useful for the synthesizer.  There are four pots, used as follows:

  • Pot 1: Select wave – choose between sine, square, sawtooth, triangle.
  • Pot 2: Select intensity – sets a floating point value between 0.0 and 1.0 based on the pot reading to control the “strength” of the modulations.
  • Pot 3: Set the modulation intensity rate – a floating point value between 0.0 and 10.24 based on the pot reading.
  • Pot 4: Set the modulation ratio.  Chooses one of 8 pre-set values (1, 2, 3, 5, 7, 9, 11, 13) for the frequency multiplier for the second oscillator.

The oscillator class allows a modulator to be defined on initialisation.  I’ve set up three oscillators as follows:

  • An intensity oscillator set using the modulation rate to between 0.0 (disabled) and 10.24 Hz.
  • A carrier oscillator (VCO1) optionally modulated by VCO2.
  • A modulator oscillator (VCO2) optionally modulated b the intensity oscillator.

In the GetChunk method, the following happens:

Set VCO1 waveform
Set VCO2 waveform
Set VCO1 modulation volume based on intensity pot value
Set VCO2 modulation volume based on intensity pot value
Set VCO1 frequency from the playing note frequency
Set VCO2 frequency from the playing note frequency * mod ratio

Fill in a buffer for this "chunk" of samples:
   IF (key pressed) THEN
      Update NextSample for Intensity modulator
      Update NextSample for VCO2
      Update NextSample for VCO1

      Set the current sample from VCO1.GetOutputLevel()

I experimented with a number of ways of combining the oscillators.  Ideally I wanted to emulate the FM synthesis modes from Mozzi, but I’m not quite there yet, but as you can hopefully hear in the video this is producing some interesting results already.  I need to go back to first principles I think to read once again about frequency modulation as it applies to sound synthesis.

Note: It is possible to configure this to use I2S audio by uncommenting the following in circlesynth.h:

// define only one
#define USE_I2S
//#define USE_HDMI

Alternatively, just keep using the headphone output.

Or of course, you could just jump ahead to one of the “off the shelf” bare metal Raspberry Pi synths out there!

Find it on GitHub here.

Closing Thoughts

This was a little fiddly to get to this point and as I say this is far from the finished article, but it is not a bad start.  I have MIDI, several controllable synthesis parameters and some interesting noises! But there is still a long way to go.  You can hear the results so far, along with a little weirdness here and there, in the video.

Further developments will probably include:

  • Some kind of potentiometer hat or shield to make controlling things a bit easier.
  • Possibly an interface with my Clumsy MIDI shield for the MIDI side of things.
  • More IO experiments (buttons, other inputs, possibly even some kind of display).
  • Ultimately taking one of the existing bare metal synths and seeing where I can go with it.

Update: I’ve now tested this with my Raspberry Pi v1 Model B Synth Board.

Kevin

Leave a comment