Skip to content

Instantly share code, notes, and snippets.

@rbnpi
Last active December 16, 2018 22:28
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save rbnpi/9e1c66bfc41189d9ccf88622b26bd3ec to your computer and use it in GitHub Desktop.
Sonic Pi and Spirograph (using python3) See article at https://rbnrpi.wordpress.com/project-list/sonic-pi-and-spirograph/ video at https://youtu.be/uzD9HEr4Cl4
#updated to python3 by Robin Newman December 2018
#function that finds greatest common divisor using Euclidian algorithm.
#For math description of the algorithm, visit https://en.wikipedia.org/wiki/Euclidean_algorithm
def euclidianGCD(a, b):
#check if either of the inputs is zero
if (a==0 or b==0):
if (a==0 and b!=0):
return b
elif (a!=0 and b==0):
return a
else:#if both are zero
print("Cannot return a common divisor when both inputs are zero. Return None.")
return None
else:#if neither input is zero
a_new = abs(a-b)
b_new = min(a, b)
while (a_new != b_new):
this_a = a_new
this_b = b_new
a_new = abs(this_a - this_b)
b_new = min(this_a, this_b)
return a_new #alternatively, might return b_new since they are the same thing with a_new
#python3 version (same as python2 version)
#analog of the range() function but this one allows decimal increments
def frange(start, end, step=None):
if step!=None:
this = start
list = []
list.append(this)
while (this <= end):
this = this + step
this = round(this, 2)
list.append(this)
return list
#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] #ssend 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
#Author: marktini github.com/marktini
#Spirograph curve drawing program
#Version 1.0
#modified to Python3 by Robin Newman Dec 2018
#additions to allow export of x,y coords using OSC message
#with aid of pythonosc library
import math
import turtle
import random
import time
from euclidian import euclidianGCD
from frange import frange
from pythonosc import osc_message_builder
from pythonosc import udp_client
class Spirograph:
#set radius of the spirograph toy (outer circle)
def __init__ (self, R):
self.R = R
self.t = turtle.Turtle() #to be able to access turtle from different methods
#set radius of the inner circle
def setSmallCircle(self, r):
self.r = r
#set distance of pen from inner circle radius; set pen color
def setPen(self, d, color='black', random=False):
self.d = d
self.color = color
self.random = random
#draw the spirograph given current settings
def draw(self):
#find greatest common denominator of r and R using Euclidian algorithm:
gcd = euclidianGCD(self.r, self.R)
#number of periods is the reduced numerator of the fraction r/R
numPeriods = self.r/gcd
numPetals = self.R/gcd
#calculate constants for graphing
print('Periods: ', numPeriods)
print('Petals: ', numPetals)
k = float(self.r)/self.R
l = float(self.d)/self.r
print('k=',self.r, '/',self.R, '=',k,' l=',self.d,'/',self.r,'=',l)
#use the custom-made frange function to make a list of angles of given increment
angleIncrement = 0.01 #the smaller angleIncrement, the more data points
ptsPeriod = math.ceil(2*math.pi/angleIncrement)
print("Points per Period: ", ptsPeriod)
#frange function is an alternative to range(). The last argument specifies a decimal step
angles = frange(0, 2*math.pi*numPeriods, angleIncrement)
xCoordinates = []
yCoordinates = []
#calculate all the (x,y) points corresponding to parameters in the "angles" list
for theta in angles:
thisX = self.R*((1-k)*math.cos(theta) + l*k*math.cos((1-k)/(k)*(theta)))
thisY = self.R*((1-k)*math.sin(theta) + l*k*math.sin((1-k)/(k)*(theta)))
xCoordinates.append(thisX)
yCoordinates.append(thisY)
print('Num data points: ', len(xCoordinates))
t = self.t #for brevity in future references to the turtle
screen= t.getscreen() #same as above
screen.bgcolor("black")
#name the canvas window
title = "Spirograph with R= " + str(self.R) + ", r = "+str(self.r) + ", and d = " +str(self.d)
screen.title(title)
#for the first point, just move the pen without leaving trace
t.up()
t.goto(xCoordinates[0], yCoordinates[0])
t.down()
#speed up the drawing! update every 20 points. Change these parameters to vary speed
screen.tracer(20)
t.speed(6)
randColors = False #if True, change up colors randomly with each period
t.color(self.color)
sender=udp_client.SimpleUDPClient("127.0.0.1",4559)
pointsCount = 0
for each in range(len(xCoordinates)):
t.goto(xCoordinates[each], yCoordinates[each])
pointsCount = pointsCount + 1
#additonal section to export x.y coords using OSC message
if (pointsCount % (numPetals*2) == 0):
sender.send_message('/xcoord',xCoordinates[each])
if (pointsCount % (numPetals*4) ==0):
sender.send_message('/ycoord',yCoordinates[each])
#end of additional section
if (randColors):
if (pointsCount % ptsPeriod == 0):
red = random.random()
green = random.random()
blue = random.random()
t.color(red, green, blue)
t.hideturtle()
print("Done drawing this curve")
time.sleep(2)
sender.send_message("/finished","done")
#clear the drawing surface after "sec" seconds. Limit time to 2 min just in case
def clear(self, sec):
if sec > 2*60:#reset time in case it's too long
sec = 10
time.sleep(sec)
self.t.getscreen().reset() #now reset the screen
#spiroOSCserver.py by Robin Newman, December 2018. (use python3)
from pythonosc import osc_server
from pythonosc import dispatcher
import argparse
import time
import os
from Spirograph import Spirograph
cwd = os.getcwd() #current directory
def oscTest(unused_addr,args,data):
print("data received",data)
def drawIt(unused_addr,args,r,sr,d,col):
s="/usr/local/bin/python3 "+cwd+"/spiroRun.py -r "+r+" -sr "+sr+" -d "+d+" -col '"+col+"'"
os.system(s)
if __name__ == "__main__":
try:
parser = argparse.ArgumentParser()
parser.add_argument("-ip",
default = '127.0.0.1', help="The ip of this computer")
parser.add_argument("-sp",
default = '127.0.0.1', help="The ip of the computer running Sonic Pi")
#This is the port on which the server listens. Usually 8000 is OK
#but you can specify a different one
parser.add_argument("--port",
type=int, default=8000, help="The port to listen on")
args=parser.parse_args()
spip=args.sp
#######dispatchers which handle incoming osc calls. They pass the data received on to
####### the routine following the OSC address. eg "/setAll" calls oscSetAll with data cname and id
####### All routines can send optional message back to Sonic Pi if id param is > -1 (default),sender
####### steps, wait_ms and id all have default values if omitted
dispatcher = dispatcher.Dispatcher()
dispatcher.map("/test",oscTest,"data") #for testing purposes only
dispatcher.map("/draw",drawIt,"r","sr","d","col")
#now setup sender to return OSC messages to Sonic Pi
print("Sonic Pi on ip",spip)
#sender set up for specified IP
#Now set up and run the OSC server
server = osc_server.ThreadingOSCUDPServer(
(args.ip, args.port), dispatcher)
print("Serving on {}".format(server.server_address))
#run the server "forever" (till stopped by pressing ctrl-C)
server.serve_forever()
except KeyboardInterrupt:
print("exiting program")
#spiroRun.py by Robin Newman, December 2018
from Spirograph import Spirograph
import argparse
import time
parser = argparse.ArgumentParser()
parser.add_argument("-r")
parser.add_argument("-sr")
parser.add_argument("-d")
parser.add_argument("-col")
args=parser.parse_args()
r=int(args.r)
sr=int(args.sr)
d=int(args.d)
col=args.col
s = Spirograph(r)
s.setSmallCircle(sr)
s.setPen(d, col)
s.draw()
time.sleep(2)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment