Skip to content

Instantly share code, notes, and snippets.

# rbnpi/Spirograph.py Last active Dec 16, 2018

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] #store large circle radius set :s,p[x] #store current synth osc "/draw",p[x],p[x],p[x],p[x] #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,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,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, yCoordinates) 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)
to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.