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…
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
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