Callum Kirkwood

Blog
Projects
Photography
LinkedIn
GitHub
Twitter

Sonic Pi I/O, part 1 - Inputs

28 Jul 2017

McPi

Source: Giphy

I’ve spent about a week with Sonic Pi 3 so far, the latest version of the live coding synth. Already, it’s a gamechanger for me - I quickly managed to replicate my base Ableton Live rig in about 160 lines of code (including a looper), and the live audio input is so simple to use. I spent the first few days getting to grips with MIDI control and live audio effects, until I realised what was possible with OSC (Open Sound Control).

Thanks to OSC it’s now possible to send or receive short messages to remote devices, which means that pretty much anything that a Raspberry Pi can do can be controlled by Sonic Pi. I’ve always planned to build some form of input device/instrument, but now I’ll definitely be adding lights as well as buttons, pots and sliders…

Analogue Inputs

Sending values to Sonic Pi is surprisingly simple. A client script runs on a remote Raspberry Pi (or any device/language that supports OSC), which calls functions that contain the input commands.

Making use of a previous build I was able to send values from analogue pots to control parameters in Sonic Pi, although this would work with anything using an MCP3008 analogue-to-digital converter.

I found that PyOSC is one of the easier libraries to work with, but there are a few options out there. Basic setup of the client is as follows:

import OSC
import time
import Adafruit_MCP3008

# set ip address and port of the machine running Sonic Pi
send_address = (ip.address, 4559)

# Initialize the OSC client.
c = OSC.OSCClient()
c.connect(send_address)

Next, define the pins used by the MCP3008:

CLK = 11
MISO = 9
MOSI = 10
CS = 8
mcp = Adafruit_MCP3008.MCP3008(clk=CLK, cs=CS, miso=MISO, mosi=MOSI)

Third, the functions:

# create a function to send multiple arguments in one message
def send_osc(addr, *stuff):
    msg = OSC.OSCMessage()
    msg.setAddress(addr)
    for item in stuff:
        msg.append(item)
    c.send(msg)

# function to read ADC values and send them to Sonic PI
def pot_value():
    while True:
	a = (((mcp.read_adc(0) - 0) * (127 - 0)) / (1023 - 0)) + 0
	b  = (((mcp.read_adc(1) - 0) * (3 - 0)) / (1023 - 0)) + 0
	c = (((mcp.read_adc(2) - 0) * (127 - 0)) / (1023 - 0)) + 0
    	list = [a, b, c]
        send_osc('/pot/value', list)

The formula in pot_value() simply adapts the range of values to something we can use; default is 0-1023, but in this case I want to mimic MIDI values (0-127). Playing around with synth parameters in Sonic Pi I noticed that 3 different waves can be applied to :tb303, which is why the 2nd pot only goes up to 3. The address given after send_osc can be just one word, but must start with / - this will be called by Sonic Pi to retrieve the message.

Finally, loop the pot_value() function to send values to Sonic Pi that will update as you turn the pots:

try:
    while True:
        pot_value()
        time.sleep(0.1)

# clean exit
except KeyboardInterrupt:
    print 'Closing...'

Switch to another computer running Sonic Pi 3 on the same WiFi network as the client. Open Preferences > IO and tick ‘Receive remote OSC messages’ to reveal the IP and port required by the client - add these details to the client script back on the Pi, and run it.

In a new buffer, use this code (or something similar) to control a synth with 3 potentiometers - note that the sync address will need to match whatever you’ve used after send_osc in the client.

live_loop :potSynth do
  use_real_time
  a, b, c = sync '/osc/pot/value'
  synth :tb303, note: a, wave: b, pulse_width: (c/158.75)+0.1, sustain: 0.5
  sleep 0.1
end

View on GitHub

Pimoroni HATs/PHATs

Now that we have a basic client script, we just need to change the functions to enable new types of input. Pimoroni’s HATs ship with incredibly simple functions and documentation, so they can be used to control Sonic Pi with very little effort.

So far I’ve written an example for the Touch Phat, and an output example using the Unicorn Phat that I’ll break down in part 2. Simply take the basic client setup, add the relevant library to the imports, and usually you’d just create a function that drives the basic functionality of the HAT. The Touch Phat in particular acts a little differently, where the touch events are set up as normal and each pad triggers an OSC message containing a MIDI note value.

import touchphat

# confirm script is running by cycling LEDs
def initialise():
    for pad in ['Back','A','B','C','D','Enter']:
        touchphat.set_led(pad, True)
        time.sleep(0.1)
        touchphat.set_led(pad, False)
        time.sleep(0.1)

initialise()

# assign values to touch pads
@touchphat.on_touch(['Back','A','B','C','D','Enter'])
def touch_keys(event):
    if event.name == 'Back':
        send_osc('/touch/trigger', 62)
    elif event.name == 'A':
        send_osc('/touch/trigger', 64)
    elif event.name == 'B':
        send_osc('/touch/trigger', 66)
    elif event.name == 'C':
        send_osc('/touch/trigger', 68)
    elif event.name == 'D':
        send_osc('/touch/trigger', 70)
    elif event.name == 'Enter':
        send_osc('/touch/trigger', 72)

# clean exit
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    print 'Closing...'

Sonic Pi then acts on the incoming triggers like this:

live_loop :touch do
  use_real_time
  a, b, c = sync '/osc/touch/trigger'
  synth :prophet, note: a
end

View on GitHub

Coming up: Triggering LEDs with OSC