====== schallwerkzeuge.py ====== #!/usr/bin/python # coding=utf8 ## Verhalten von / wie in Python 3 a/b = float from __future__ import division from math import pi ### Audio-Module import pyaudio from scipy.io import wavfile ### Fourier, Numerik import numpy.fft as FFT import numpy as np ############## # Graphik import matplotlib.pyplot as plt ### CHANNELS=1 RATE=44100 DAUER=1 ANZAHL=DAUER*RATE # ANZAHL der Frames, # globale Variablen, schlechter Programmierstil, hier aber ganz # praktisch: Nach Aufnahme oder Lesen werden diese Variablen # entsprechend gesetzt. def recordsnd(filename, time): ''' Nimmt eine time Sekunden lange Schallsequenz auf (mit der Samplerate 48000 Hz) auf und speichert, speichert sie in der Datei filename und gibt die Aufnahme als numpy-array mit Werten zwischen -1 und 1 zurück ''' global ANZAHL global RATE global DAUER global CHANNELS DAUER=time ANZAHL=int(RATE*DAUER) p = pyaudio.PyAudio() raw_input("Aufnehmen "+str(time)+ " Sekunden): Eingabetaste drücken...") stream = p.open(format =pyaudio.paInt16 , channels = CHANNELS, rate = RATE, input = True, frames_per_buffer = 1024)#ANZAHL) yy=stream.read(ANZAHL) y=np.fromstring(yy,dtype=np.short) if CHANNELS==2: y=y.reshape((y.shape[0]//2,2)) print("Aufgenomen: " + str(ANZAHL) + " Frames") stream.close() p.terminate() yy=np.array(y,dtype='d')/32768.0 if filename!=None: wavwrite(filename, yy) return yy def wavwrite(filename, y): '''Schreibt ein wav-File aus einem numpy-array''' wavfile.write(filename, RATE, np.array(y*2**15, dtype=int)) def wavread(filename): '''Liest ein wav-File in ein numpy-Array''' global ANZAHL global RATE global DAUER global CHANNELS RATE,y=wavfile.read(filename) ANZAHL=y.shape[0] if len(y.shape)==2: CHANNELS=y.shape[1] else: CHANNELS=1 DAUER=ANZAHL/RATE return np.array(y,dtype=np.float)/2**15 def playsnd(y, r): ''' spielt das numpy-array (bzw. Vektor) y als Klang mit der Samplerate r [Hz] ab. ''' def callback(in_data, frame_count, time_info, status): data = (32767*klanggen.generate(frame_count/RATE)).astype(np.int16) return (data.copy(), pyaudio.paContinue) p = pyaudio.PyAudio() CHUNK=2**12 stream = p.open(format =pyaudio.paInt16 , channels = CHANNELS, rate = r, output = True, frames_per_buffer=CHUNK) #callback=callback)#ANZAHL) yy=np.array(y.flatten()*2**15,dtype=np.short) for i in range(0,len(yy),CHUNK): stream.write(yy[i:i+CHUNK].tostring(order='F')) stream.close() p.terminate() def inspectsnd(y): '''Zeigt einen Plot von y''' n=y.shape[0] fig=plt.Figure(figsize=(6,4),dpi=100) ax=fig.add_subplot(111) x=np.arange(0,n,1)/np.float32(RATE) plt.plot(x,y) plt.show() def inspectspec(y): '''zeigt einen Plot des DFT-Spektrums von y''' global RATE # Vorbereitungen, um ein Fenster zum Plotten zu kriegen plt.figure(1) n=len(y) window=np.blackman(n) sumw=sum(window*window) A=FFT.fft(y*window) B2=(A*np.conjugate(A)).real sumw*=2.0 sumw/=1/np.float(RATE) # sample rate B2=B2/sumw x=np.arange(0,n/2,1)/np.float(n)*RATE eps=1e-8 plt.plot(x, np.log10(B2[0:n/2]+eps)) plt.show() def sinewave(f,r,d): '''erzeugt die Daten einer Sinusschwingung mit Frequenz f, Dauer d und Samplerate r''' global RATE global DAUER global ANZAHL RATE=r DAUER=d ANZAHL=DAUER*RATE y=np.zeros(r*d) for i in range(0,r*d): y[i]=np.sin(2*pi*f*i/float(r)) return y ====== fouriersb.py ====== from __future__ import division # -*- coding: utf-8 -*- import scipy import numpy.fft as FFT import matplotlib.pyplot as plt from matplotlib.ticker import FormatStrFormatter execfile("schallwerkzeuge.py") def calc_hamming_weights(T,fs,framesamp, hopsamp): halfwindow=framesamp//2 x=scipy.zeros(int(T*fs)) w = scipy.hamming(framesamp) for i in range(halfwindow, len(x)-halfwindow, hopsamp): x[i-halfwindow:i+halfwindow+1] += w for i in range(0,int(T*fs)): if x[i]!=0: x[i]=1./x[i] else: x[i]=1. return x def stft(x, fs, framesz, hop): """Short time Fourier transformation: stft(x, RATE, fensterdauer, fensterverschiebung) Berechnet die diskrete Fourier-Transformation jeweils von einem Signalstück der Dauer "fensterdauer". Die betrachteten Stücke sind jeweils um "fensterverschiebung" gegeneinander verschoben. Vor der Transformation werden sie mit einem Hamming-Window multipliziert. Die Funktion liefert ein Array zurück, dessen Zeilen die einzelnen Fouriertransformationen enthalten. """ #x=x.copy() halfwindow = int(framesz*fs/2) framesamp=2*halfwindow+1 hopsamp = int(hop*fs) w = scipy.hamming(framesamp) X = scipy.array([scipy.fft(w*x[i-halfwindow:i+halfwindow+1]) for i in range(halfwindow, len(x)-halfwindow, hopsamp)]) return X def istft(X, fs, T, hop): """ istft(X, RATE, DAUER, fensterverschiebung) Umkehrung von stft, nicht ganz perfekt. Interpretiert die Matrix X so, dass die Zeilen die Fouriertransformierte von Stücken enthalten, die jeweils um "fensterverschiebung" gegeneinander verschoben sind. """ print fs, T, int(T*fs) x = np.zeros(int(T*fs),dtype=np.complex) framesamp = X.shape[1] halfwindow=framesamp//2 hopsamp = int(hop*fs) w=calc_hamming_weights(T,fs,framesamp,hopsamp) #ww = scipy.hamming(framesamp) #for i in range(len(ww)): # try: # ww[i]=1./ww[i] # except: # ww[i]=1. #print "halfwindow " , halfwindow, " hopsamp ", hopsamp , "len(x)", len(x) for n,i in enumerate(range(halfwindow, len(x)-halfwindow, hopsamp)): #print "n: ",n, "i: ", i#print len(FFT.ifft(X[n])) #print len(x[i-halfwindow:i+halfwindow+1]) x[i-halfwindow:i+halfwindow+1] += scipy.ifft(X[n]) return x*w def ersterplot(filename): global DAUER global RATE global ANZAHL sig=wavread(filename) plt.subplot(2,2,1) timescale=linspace(0,DAUER,len(sig)) plt.plot(timescale,sig) plt.xlabel("Zeit [s]") plt.ylabel("Amplitude des Signals") plt.title("Das Signal") plt.subplot(2,2,2) plt.plot(timescale,scipy.bartlett(len(sig))) plt.xlabel("Zeit [s]") plt.ylabel("Faktor") plt.title("Das Bartlett-Fenster") sigf=FFT.fft(sig) plt.subplot(2,2,3) frqscale=linspace(0,len(sig)/DAUER, len(sig)) plt.plot(frqscale,np.abs(sigf)) plt.xlabel("Frequenz [Hz]") plt.ylabel("Amplitude ") plt.title("Das Spektrum") w=scipy.bartlett(len(sig)) sigfhanning=FFT.fft(w*sig) plt.subplot(2,2,4) plt.plot(frqscale,np.abs(sigfhanning)) plt.xlabel("Frequenz [Hz]") plt.ylabel("Amplitude ") plt.title("Spektrum mit Bartlett-Fenster") plt.show() def zweiterplot(): # hier nun wird etwas ausprobiert, das Ihr letztes Mal # angesprochen habt: Immer in kleinen Zeitintervallen # das Spektrum berechnen global DAUER global RATE global ANZAHL fensterdauer=0.2 fensterueberlappung=0.05 # jeweils in Sekunden sig=wavread("lala.wav") A=stft(sig,RATE,fensterdauer,fensterueberlappung) rsig=istft(A,RATE,DAUER,fensterueberlappung) eps=1e-8 # Offset, um logarithmieren zu koennen r,s=A.shape yl=linspace(0,DAUER, r) xl=linspace(0,RATE/2,s/2) X,Y=meshgrid(xl,yl) plt.figure(1) plt.pcolor(X,Y,np.log10(np.absolute(A[:,:s/2]+eps))) plt.show() def dritterplot(): global DAUER global RATE global ANZAHL # Hier vergleichen wir die Rekonstruktion mit dem # ursprünglichen Signal fensterdauer=0.2 fensterueberlappung=0.05 # jeweils in Sekunden sig=wavread("lala.wav") A=stft(sig,RATE,fensterdauer,fensterueberlappung) rsig=istft(A,RATE,DAUER,fensterueberlappung) plt.subplot(2,1,1) plt.plot(np.abs(sig)) plt.subplot(2,1,2) plt.plot(np.abs(rsig)) print len(sig), len(rsig) plt.show() def vierterplot(fensterdauer=0.2,fensterueberlappung=0.05): global DAUER global RATE global ANZAHL # # Heisenbergsche Unschärferelation # sig=np.append(sinewave(200,RATE,1),sinewave(500,RATE,1)) DAUER=2 A=stft(sig,RATE,fensterdauer,fensterueberlappung) jump=int(fensterdauer/2*RATE/(DAUER/fensterueberlappung)) eps=1e-8 # Offset, um logarithmieren zu koennen r,s=A.shape yl=linspace(0,DAUER, r) xl=linspace(0,RATE/2,s/2) X,Y=meshgrid(xl,yl) plt.figure(1) plt.pcolor(X,Y,np.log10(np.absolute(A[:,:s/2]+eps))) plt.show() def fuenfterplot(): global DAUER global RATE global ANZAHL fensterdauer=0.2 fensterueberlappung=0.05 # jeweils in Sekunden eps=1e-8 # Offset, um logarithmieren zu koennen # # Nun die Spektren von i und a zum Vergleich # sig1=wavread("a.wav") A1=stft(sig1,RATE,fensterdauer,fensterueberlappung) sig2=wavread("i.wav") A2=stft(sig2,RATE,fensterdauer,fensterueberlappung) r,s=A1.shape yl=linspace(0,DAUER, r) xl=linspace(0,RATE/2,s/2) X,Y=meshgrid(xl,yl) plt.subplot(2,1,1) plt.pcolor(X,Y,np.log10(np.absolute(A1[:,:s/2]+eps))) r,s=A2.shape yl=linspace(0,DAUER, r) xl=linspace(0,RATE/2,s/2) X,Y=meshgrid(xl,yl) plt.subplot(2,1,2) plt.pcolor(X,Y,np.log10(np.absolute(A2[:,:s/2]+eps))) plt.show() if __name__=="__main__": ersterplot("zweisignale.wav") #ersterplot("nochzweisignale.wav") #zweiterplot() #dritterplot() #vierterplot(fensterdauer=0.2, fensterueberlappung=0.05) #vierterplot(fensterdauer=0.05, fensterueberlappung=0.02) #fuenfterplot() ### jetzt wärt ihr dran ====== microlistener.py ====== #!/usr/bin/python # coding=utf8 from __future__ import division import pyaudio import time ### Diese Klasse wird im Modul microlistener definiert. ### Sie kann in allen Projekten verwendet werden, die eine ### kontinuierliche Audio-Eingabe brauchen. class MicroListener(object): def __init__(self,rate,channels,chunk,callback,playback=False): '''kreiert einen Stream, der mit der Samplerate 'rate' und 'channels' Kanälen über das Microphon Audio aufnimmt. Nach jeweils 'chunk' Frames wird die callback-Funktion aufgerufen. Falls playback==True, werden die Daten an den Lautsprecher durchgereicht. Die Callback-Funktion nimmt als Argumente (in_data, frame_count, time_info, status) und gibt ein Tupel zurück, dessen erster Eintrag die Daten (für Playback), dessen zweiter Eintrag ein Signal ist, sollte pyaudio.paContinue sein. Also z. B. (mit einer globalen Variable CHANNELS) def micro_callback(in_data, frame_count, time_info) y=np.fromstring(in_data,dtype=np.short) if CHANNELS==2: y=y.reshape((y.shape[0]//2,2)) else: y=y.reshape((y.shape[0],1)) ## y enthält danach die Daten des letzten Abschnitts ## aus chunk samples. ## jetzt irgendwas mit den Daten machen ## return (in_data, pyaudio.paContinue) ''' self.p = pyaudio.PyAudio() self.stream = self.p.open(format=pyaudio.paInt16 , channels=channels, rate=rate, input=True, output=playback, stream_callback=callback, frames_per_buffer=chunk) self.stream.start_stream() time.sleep(0.5) def __del__(self): self.stream.stop_stream() self.stream.close() self.p.terminate() ######################################################################### ### Test-Sektion ####################################################### ######################################################################### if __name__=='__main__': import numpy as np ########################### ### GLOBALE VARIABLEN CHANNELS=2 RATE=44100 CHUNK=2**11 def micro_callback(in_data, frame_count, time_info,status): '''callback Funktion: wird vom PyAudio-Objekt aufgerufen, wenn CHUNK Frames von der Soundkarte (Mikrophon) gelesen wurden''' y=np.array(np.fromstring(in_data,dtype=np.short),dtype=np.float) if CHANNELS==2: y=y.reshape((y.shape[0]//2,2)) else: y=y.reshape((y.shape[0],1)) print int(np.linalg.norm(y[:,0])/2000.)*'*' return (in_data, status) listen=MicroListener(RATE,CHANNELS,CHUNK,micro_callback) while True: pass ====== microlistenerdftnew.py ====== #!/usr/bin/python # coding=utf8 from __future__ import division import pyaudio import time ### Diese Klasse wird im Modul microlistener definiert. ### Sie kann in allen Projekten verwendet werden, die eine ### kontinuierliche Audio-Eingabe brauchen. class MicroListener(object): def __init__(self,rate,channels,chunk,callback,playback=False): '''kreiert einen Stream, der mit der Samplerate 'rate' und 'channels' Kanälen über das Microphon Audio aufnimmt. Nach jeweils 'chunk' Frames wird die callback-Funktion aufgerufen. Falls playback==True, werden die Daten an den Lautsprecher durchgereicht. Die Callback-Funktion nimmt als Argumente (in_data, frame_count, time_info, status) und gibt ein Tupel zurück, dessen erster Eintrag die Daten (für Playback), dessen zweiter Eintrag ein Signal ist, sollte pyaudio.paContinue sein. Also z. B. (mit einer globalen Variable CHANNELS) def micro_callback(in_data, frame_count, time_info) y=np.fromstring(in_data,dtype=np.short) if CHANNELS==2: y=y.reshape((y.shape[0]//2,2)) else: y=y.reshape((y.shape[0],1)) ## y enthält danach die Daten des letzten Abschnitts ## aus chunk samples. ## jetzt irgendwas mit den Daten machen ## return (in_data, pyaudio.paContinue) ''' self.p = pyaudio.PyAudio() self.stream = self.p.open(format=pyaudio.paInt16 , channels=channels, rate=rate, input=True, output=playback, stream_callback=callback, frames_per_buffer=chunk) self.stream.start_stream() time.sleep(0.5) def __del__(self): self.stream.stop_stream() self.stream.close() self.p.terminate() ######################################################################### ### Test-Sektion ####################################################### ######################################################################### if __name__=='__main__': import numpy as np import matplotlib.pyplot as plt import scipy.fftpack plt.ion() ########################### ### GLOBALE VARIABLEN CHANNELS=1 RATE=44100 CHUNK=2**11 fig,ax = plt.subplots(1,1) graf, = plt.plot(np.linspace(0.0, (2.0*RATE), CHUNK/2),(CHUNK//2-1)*[0]+[100000]) def micro_callback(in_data, frame_count, time_info,status): '''callback Funktion: wird vom PyAudio-Objekt aufgerufen, wenn CHUNK Frames von der Soundkarte (Mikrophon) gelesen wurden''' y=np.array(np.fromstring(in_data,dtype=np.short),dtype=np.float) yf = scipy.fftpack.fft(y)[:CHUNK/2] xf = np.linspace(0.0, (2.0*RATE), CHUNK/2) graf.set_data(xf,np.abs(yf)) #plt.pause(0.001) #plt.show() if CHANNELS==2: y=y.reshape((y.shape[0]//2,2)) else: y=y.reshape((y.shape[0],1)) #print int(np.linalg.norm(y[:,0])/2000.)*'*' return (in_data, status) listen=MicroListener(RATE,CHANNELS,CHUNK,micro_callback) while True: fig.canvas.draw() plt.pause(0.001) ====== moving_circles.py ====== # -*- coding: utf-8 -*- # uses: # code under Copyright (c) 2014, Vispy Development Team. # Distributed under the (new) BSD License. See LICENSE.txt for more info. # # most of it: Copyright 2017, Stefan Born # # License: GPL, version 2 '''Package moving_circles provides two backends for drawing and moving circles on a canvas. You select the backend by setting the variable PREFERRED_BACKEND in the calling module to either 'VISPY' or 'MATPLOTLIB'.''' from __future__ import division # Analyse code of importing file: import inspect, re try: filename = inspect.getfile(inspect.currentframe().f_back) with open(filename, 'r') as f: importing_module = f.read() PREFERRED_BACKEND = re.search ("PREFERRED\_BACKEND\s*=\s*['\"](?P\w*)['\"]", importing_module).group('preferred') except: PREFERRED_BACKEND = 'MATPLOTLIB' # import depends on PREFERRED_BACKEND import sys import numpy as np import time AVAILABLE_BACKENDS = [] try: from vispy import app, gloo, visuals, scene from vispy.visuals import transforms AVAILABLE_BACKENDS.append('VISPY') except: pass try: import matplotlib matplotlib.use('GTKAgg') from matplotlib.backends.backend_gtkagg import FigureCanvasGTKAgg as FigureCanvas import matplotlib.figure import matplotlib.patches import matplotlib.axes import gtk AVAILABLE_BACKENDS.append('MATPLOTLIB') except Exception as e: print e if PREFERRED_BACKEND in AVAILABLE_BACKENDS: BACKEND = PREFERRED_BACKEND else: try: BACKEND = AVAILABLE_BACKENDS[0] except: raise Exception("NO BACKEND AVAILABLE") print "Backend: ", BACKEND if BACKEND == 'VISPY': class Canvas(scene.SceneCanvas): '''Canvas for drawing. Is created with a certain initial width and height. The coordinate system for Drawing (implemented: Circles) refers to xlim and ylim. :param width: integer, width of canvas :param height: integer, height of canvas :param xlim: tuple of float, limits of x-axis :param ylim: tuple of float, limits of y-axis''' def __init__(self, height=800, width=800, xlim = (-1.,1.),ylim=(-1.,1.)): scene.SceneCanvas.__init__(self, keys='interactive',bgcolor='w') self.size = (width,height) self.original_size = self.size self.visuals = [] self.xlim = xlim self.ylim = ylim self.center = (0,0) self.scale = (width/(self.xlim[1]-self.xlim[0]), height/(self.ylim[1]-self.ylim[0])) self.translate = ((-self.xlim[0])*self.scale[0],(-self.ylim[0])*self.scale[1]) self.transform = transforms.STTransform( scale = self.scale, translate = self.translate ) self.show() self.set_current() self.action = lambda x,y:0 self.objects = [] self.app.create() def on_draw(self, ev): print "ond" gloo.set_clear_color(self.bgcolor) gloo.set_viewport(0, 0, *self.size) gloo.clear() for vis in self.visuals: vis.draw(vis.tr_sys) def update_canvas(self): '''manually update canvas''' self.app.process_events() self.update() def set_action(self,action,objects): '''registers an action to be performed on certain objects at each update :param action: function (objects, event) :param objects: list of objects ''' self.action = action self.objects = objects def on_timer(self, event): self.action(self.objects,event) self.update() def on_resize(self,event): width, height = self.size self.scale = np.array((self.original_size[0]/width*self.scale[0],self.original_size[1]/height*self.scale[1],1,1)) self.transform = transforms.STTransform( scale = self.scale, translate = self.translate ) for vis in self.visuals: vis.transform = self.transform vis.tr_sys.visual_to_document = vis.transform self.original_size = self.size def run(self): self.on_draw(None) time.sleep(1) self._timer = app.Timer(interval=0.05, connect=self.on_timer, start=True) class Circle(object): def __init__(self, canvas, **params): self.params = dict( pos =canvas.center, radius = (0.1,0.1), color=(0.2, 0.2, 0.8, 1), border_color=(1, 1, 1, 1) ) for key in params: if key!='radius': self.params[key] = params[key] else: try: self.params[key] = (params[key],params[key]) except: pass self.visual = visuals.EllipseVisual(**self.params) self.visual.transform = canvas.transform self.visual.tr_sys = transforms.TransformSystem(canvas) self.visual.tr_sys.visual_to_document = self.visual.transform self.canvas = canvas canvas.visuals.append(self.visual) def set_radius(self, r): self.visual.radius = (r,r) def move(self,dx,dy): self.visual.pos = (self.visual.pos[0]+dx, self.visual.pos[1]+dy) def set_color(self, color): self.visual.color = color if BACKEND == 'MATPLOTLIB': from collections import namedtuple Event = namedtuple("Event",("dt", "elapsed")) class Canvas(object): '''Canvas for drawing. Is created with a certain initial width and height. The coordinate system for Drawing (implemented: Circles) refers to xlim and ylim. :param width: integer, width of canvas :param height: integer, height of canvas :param xlim: tuple of float, limits of x-axis :param ylim: tuple of float, limits of y-axis''' def __init__(self, height=800, width=800, xlim = (-1,1),ylim=(-1,1)): self.figure = matplotlib.figure.Figure() self.figure.add_axes((0,0,1,1)) self.axes = self.figure.axes[0] self.canvas = FigureCanvas(self.figure) self.xlim = xlim self.ylim = ylim self.axes.set_xlim(xlim) self.axes.set_ylim(ylim) self.size = (width,height) self.original_size = (width, height) self.gtkwin = gtk.Window() self.gtkwin.set_default_size(width, height) self.gtkwin.add(self.canvas) self.gtkwin.show_all() self.action = lambda x,y:0 self.objects = [] self.visuals = [] self.center = (0,0) self.start_time = time.time() self.last_time = time.time() self.timer = self.canvas.new_timer(interval = 50) self.timer.add_callback(self.update) self.timer.start() self.canvas.mpl_connect('resize_event', self.on_resize) def set_action(self,action,objects): '''registers an action to be performed on certain objects at each update :param action: function (objects, event) :param objects: list of objects ''' self.action = action self.objects = objects def update(self): now = time.time() self.action(self.objects,Event(dt=now-self.last_time,elapsed=now-self.start_time)) self.last_time = now self.axes.figure.canvas.draw() def update_canvas(self): '''manually update canvas''' self.update() def on_resize(self, ev): width = ev.width height = ev.height self.size = (width, height) self.xlim = ((self.xlim[0]-self.center[0])*width/self.original_size[0]+self.center[0], (self.xlim[1]-self.center[0])*width/self.original_size[0]+self.center[0]) self.ylim = ((self.ylim[0]-self.center[1])*height/self.original_size[1]+self.center[1], (self.ylim[1]-self.center[1])*height/self.original_size[1]+self.center[1]) self.original_size = self.size self.axes.set_xlim(self.xlim) self.axes.set_ylim(self.ylim) def run(self): gtk.main() class Circle(object): def __init__(self, canvas, **params): self.params = dict( pos =canvas.center, radius = 0.1, color=(0.2, 0.2, 0.8, 1), border_color=(1, 1, 1, 1)) for key in params: self.params[key] = params[key] self.visual = matplotlib.patches.Circle(self.params['pos'],radius=self.params['radius'], facecolor=self.params['color'], edgecolor = self.params['border_color']) self.canvas = canvas self.canvas.axes.add_patch(self.visual) self.canvas.visuals.append(self.visual) def move(self,dx,dy): self.visual.center = (self.visual.center[0]+dx, self.visual.center[1]+dy) def set_color(self, color): self.visual._facecolor = color def set_radius(self, r): self.visual.radius = r def action(objects, event): dt, elapsed = event.dt, event.elapsed for i,circ in enumerate(objects): circ.move(0.5*dt*np.sin(elapsed*(i+1)), 0.5*dt*np.cos(elapsed*(i+1))) circ.set_color( (np.abs(np.sin(elapsed*(i+1))),np.abs(np.sin(elapsed*2*(i+1))), np.abs(np.sin(elapsed*3*(i+1))),0.3) ) if __name__ == '__main__': print BACKEND win = Canvas() objects = [] for i in range(10): circ = Circle(win, pos=(-0.6+0.05*i,0), radius=0.1,color = (1,0,0,0.2)) objects.append(circ) win.set_action(action, objects) win.run()