A Simple Pyo Synth

How to make a simple midi-controlled synth using Pyo.

Pyo is an extremely powerful sound synthesis and processing python module. It has excellent documentation, but very little in the way of tutorial material.

So in this article I’m going to explain how to make a simple midi controlled synthesiser. This covers:

  • Getting and using MIDI input
  • Tables
  • ADSRs
  • Oscillators
  • GUIs

If you haven’t used pyo before, have a skim over the official introductory tutorial.

For this tutorial you will need:

  • Python with Pyo installed
  • A MIDI input
  • Audio out (dur)

Making the Synth

Start with the obligatory import statement:

from pyo import *

Setting Up the Server

The convention seems to be to store your server in a variable called s, and I see no reason not to stick to that.

# Set Up Server
s = Server()
s.setMidiInputDevice(2) # Must be called before s.boot()
s.boot()
s.start()

The server must be set up before it is boot()ed, and s.boot() must be called before the audio processing chain is defined.Python starts engaging with the audio drivers when s.start() is called.

Both of these tasks take a couple of seconds to run, so I tend to call them before doing anything else. Strictly speaking you can call s.start() at the end of the script and it makes no difference.

Which MIDI Device?

Calling pm_get_input_devices() in an interactive shell will give you a numbered list of MIDI inputs. The int you pass to s.setMidiInputDevice() is simply the number of the device you’re using.

For example, I’m using a Novation ReMote SL Compact. pm_get_input_devices() gives me this output:

>>> pm_get_input_devices()
(['IAC Driver Bus 1', 'IAC Driver IAC Bus 2', 'ReMOTE SL Compact Port 1', 'ReMOTE SL Compact Port 2', 'ReMOTE SL Compact Port 3'], [0, 1, 2, 3, 4])
0: IN, name: IAC Driver Bus 1, interface: CoreMIDI
1: IN, name: IAC Driver IAC Bus 2, interface: CoreMIDI
2: IN, name: ReMOTE SL Compact Port 1, interface: CoreMIDI
3: IN, name: ReMOTE SL Compact Port 2, interface: CoreMIDI
4: IN, name: ReMOTE SL Compact Port 3, interface: CoreMIDI

And I know I want MIDI from ReMOTE SL Compact Port 1, so I call:

s.setMidiInputDevice(2)

Set Up MIDI

Receiving MIDI input is very simple:

# Set Up MIDI
midi = Notein() # The defaults are more than adequate

The midi variable is an object that exposes two streams, pitch and velocity. They can be accessed via [] notation, e.g: midi['pitch']. At any given moment, the value of midi['pitch'] is zero or more integers representing midi note values.

Pyo works using a whole load of these real-time streams, as you’ll see later.

The Oscillator

For the purposes of understanding, we need to take a leap forward to the end of the script and have a look at the object that’s actually producing the sound, so you understand what it needs to be given to function properly.

The class in question is Osc, and to function it requires (at minimum) three variables:

  • A frequency
  • An amplitude (volume)
  • A waveform

So, we need to generate a waveform for the Osc to play, and from the MIDI input we need to generate streams of frequencies and amplitudes at which to play that waveform.

Handling Pitch

If you try feeding the values from midi['pitch'] into an Osc, you get some seriously microtonal music. This is because the values provided by midi['pitch'] are MIDI note numbers, but the Osc object we’ll be using to generate sound works with frequencies.

We need some way of translating the MIDI values into pitches, and the MToF (MIDI to Frequency) object does just that:

# Pitch
pitch = MToF(midi['pitch'])

So we’ve fed the pitch stream into this object, and it’s producing another stream representing exactly the same information, but encoded as frequencies instead of midi note numbers. Perfect!

Handling velocity and preventing everlasting notes

A handy way of getting extremely easy control of the amplitude of your sounds over time is to use the built-in MidiAdsr class. We’ll stick to the defaults for the moment, so all we need to do is feed in the midi data:

# ADSR
amp = MidiAdsr(midi['velocity'])

Given the stream of raw velocities, we get a stream of ADSR controlled amplitudes (values from 0-1) that we can feed into the Oscillator.

Setting Up the Oscillator

So, we have a pitch stream and a amp stream. The last bit of data we need is a waveform for the synth to play. Without going into too much detail, we need a pyo table. I’ll explain more about these in a future article, for the moment just use the built in square wave:

# Table
wave = SquareTable()

Now for the actual oscillator:

# Osc
osc = Osc(wave, freq=pitch, mul=amp)

And that’s it. If you’re following along in an interactive shell, run osc.out() and hit some notes on your keyboard — you should get some sound! (If you don’t, you’ve probably missed something, but don’t worry — there’s a full code listing at the bottom)

Extras

FX

Pyo has some awesome effects built in. They take an audio source (like the osc object we’ve made) as an input and act as an audio source themselves. Try out the reverb:

verb = Freeverb(osc).out()

GUIs

Pyo has GUIs built in for a great many of it’s objects. The one you’ll use the most is:

s.gui(locals())

Essential for non-interactive scripts, this prevents the script from quitting, provides a useful interpreter, quick record feature and an easy way to start and stop the server.

Pyo Server Control

Calling ctrl() on some objects will pop up a GUI for controlling their parameters, e.g. try:

verb.ctrl()

And twiddle the sliders whilst you’re playing. Should look like this:

Freeverb GUI

The other graphical display that is indispensable whilst using pyo tables is view(). Called on any table object, it produces a graphical representation of the waveform — brilliant when the docs say things I don’t understand like ‘Chebyshev polynomials of the first kind’. Try:

wave.view()

It should produce something like this:

An almost square wave

Putting it all together

That was all a bit disjointed, so here’s the entire script, with comments:

from pyo import *

# Set Up Server
s = Server()
s.setMidiInputDevice(2) # Change as required
s.boot()
s.start()

# Set Up MIDI
midi = Notein()

# ADSR
amp = MidiAdsr(midi['velocity'])

# Pitch
pitch = MToF(midi['pitch'])

# Table
wave = SquareTable()

# Osc
osc = Osc(wave, freq=pitch, mul=amp)

# FX
verb = Freeverb(osc).out()

### Go
osc.out()
s.gui(locals()) # Prevents immediate script termination.

And here’s a diagram of exactly what’s happening, just in case I’ve confused you:

Simple Synth Structure

Download as SVG

Names of objects are: ClassName (variable name I’ve used)

I hope you have great fun using pyo! I’ll be explaining how to do more things as I learn more myself. Please direct any corrections, comments, issues, cool tricks etc to barnaby@waterpigs.co.uk