Sonic Pi and Spirograph

VERSION 2 IS NOW AVAILABLE. SEE THIS POST

I always like an opportunity to integrate Sonic Pi with a graphical drawing process, and recently I have been looking at Spirograph designs. I found a python module written 3 years ago by marktini on github which will generate some of these curves using turtle graphics. The module was written in Python2.7 and my first task was to convert it to Python3 as I wanted to use the python-osc library (which requires Python 3) to  allow two way OSC communication with Sonic Pi.

There are basically 4 parameters required for each drawing. The radius r of a large circle, the radius sr of a small circle, the distance from the centre of this small circle when the “pen” is positioned, and the drawing colour of the pen. I wanted to hold these values in a set of list in Sonic Pi, so that it could initiate each drawing and specify the characteristics.

In the other direction, the python script has to produce a large number of data points (as x,y coordinates) which are used to control the turtle. These can be sent back to Sonic Pi using OSC calls, and used to control the generation of sounds. However there are far too many of them and so I selected subsets depending upon the number of “petal” loops produced in each drawing, and used separate streams of x and y values to control two live_loops in Sonic Pi each of which produced separate sounds.

Because of the way in which the turtle graphics screen is drawn I did not find it possible to incorporate an OSC server into the drawing package. Instead I resorted to the strategy of using a total of three python scripts. The first one, called Spirograph.py was my python3 version of the original script by marktini ( with its two required files euclidian.py and frange.py which were also converted to python3). I used an intermediate script called spiroRun.py which accepted four arguments and then initiated calls to the spirograph script. The third script called spirographOSCserver.py set up an OSC server to respond to messages sent from Sonic Pi. These contained the four data items required for each drawing. When received, they are assembled into a command string , and then an os.system command is issued to start the spiroRun script.

s="/usr/local/bin/python3 "+cwd+"/spirorun.py -r "+r+" -sr "+sr+" -d "+d+" -col '"+col+"'"
os.system(s)

The values for r, sr, d and col are obtained from the OSC message sent from Sonic Pi They are picked up by the spiroRun script when it is started using the os.system call.

At the Sonic Pi end I used the following script.

#spiro.rb
#Spirograph controlled by Sonic Pi written by Robin Newman, December 2018
use_debug false
use_osc_logging false
use_cue_logging false
use_osc "localhost",8000
define :scx do |n,r| #get note to play based on circle radius r and x coordinate n
  return scale(:c4,:major,num_octaves: 2)[(n.abs/r.to_f*15).to_int]
end

define :scy do |n,r| #get selection index based on circle radius r and y coordinate n
  return (n.abs/r.to_f*4).to_i
end

set :v,1 #initial volume index value

#p contains data list for 7 drawings
p=[["250","105","175","purple","saw"],["300","187","203","yellow","tri"],
   ["300","103","201","blue","sine"],["322","63","87","purple","tri"],
   ["309","351","300","forest green","saw"],["272","107","109","cyan","pulse"],
   ["180","67","100","red","piano"]]

#main control thread to start each drawing, then wait for drawing to finish
in_thread do
  p.length.times do |x|
    set :r,p[x][0] #store large circle radius
    set :s,p[x][4] #store current synth
    osc "/draw",p[x][0],p[x][1],p[x][2],p[x][3] #send data for next drawing
    b = sync "/osc*/finished" #wait for drawing to finish
  end
end


#Playing section. Responds to OSC messages from spirograph.py
with_fx :reverb do
  live_loop :plx do
    use_real_time
    n = sync "/osc*/xcoord" #use osc* so works with SP 3.1 and 3.2dev
    use_transpose [-17,-12,0,7,12,19][get(:v)]
    synth get(:s),note: scx(n[0],get(:r)),attack: 0.05,release: 0.2,pan: [-0.8,0.8].choose,amp: [0.3,0.5,0.7,0.8,1][get(:v)]
  end
  live_loop :ply do
    use_real_time
    n = sync "/osc*/ycoord" #trigger sample when y coord received
    i = scy(n[0],get(:r))
    set :v,i #adjust volume for plx loop
    sample sample_names([:drum,:elec].choose ).choose,amp: 2,pan: [-1,0,1].choose
  end
end

Notes: This system has only been tested on my MacBook. You need a fairly hefty computer to handle the graphics. You will have to install python-osc using pip3 install python-osc
Once the system is working, you can experiment with different for the parameters and produce your own designs

You can download the code from here

You can watch a video of the program here