The goal of this notebook is to show how Csound control signals can be seen in real-time in the Python Matplotlib using the Animation module. This can be quite instructive for teaching Csound. Written by Joachim Heintz, August 2019.
Not every matplotlib backend is capable to show animations. At the time of writing this notebook, the option %matplolib inline
can only diplay static images. Use %matplotlib
instead:
In [1]:
%matplotlib qt5
The backend Qt5Agg
is — as well as some others — capable to show animations. (If necessary, you should be able to choose an appropriate backend by editing your matplotlib.rc file.)
For a basic animation using the FuncAnimation we only need two elements:
The figure can be created in many ways in matplotlib. I choose here the subplots() function in pyplot. It returns a figure and an axes object. The figure object is needed as input for the FuncAnimation. The axes object is modified by some settings, and the method plot
returns a Line2D object which will then be modified during the animation.
The animation function is updated in the call to FuncAnimation every interval
(default=200) milliseconds. The variable i
in this function is a frame counter, starting from zero.
In [2]:
from matplotlib import pyplot as plt
from matplotlib import animation
fig, ax = plt.subplots()
ax.set(xlim=(0,5), ylim=(0,1))
line, = ax.plot([], [], lw=2)
def animate(i, x=[], y=[]):
x.append(i/10)
y.append(i/50)
line.set_data(x, y)
anim = animation.FuncAnimation(fig, animate, interval=100)
You should see a line which starts at (0,0) and moves in five seconds to (5,1).
If we want to reproduce this very basic example by using a Csound control signal rather than the y-signal generated in the animate function, we have to do this:
The crucial point here is to run the csound instance in a way that it does not block the execution of the animation. This can be easily done in the way which is shown by François Pinot in the threading notebook.
Note: close the precedent graphics canvas window before lauching the next example.
In [3]:
import ctcsound as csound
from matplotlib import pyplot as plt
from matplotlib import animation
orc = '''
instr 1
kVal linseg 0, p3, 1
chnset kVal, "val"
endin
'''
sco = "i1 0 5\n" #try 0.2 as start instead
cs = csound.Csound()
cs.setOption('-odac')
cs.compileOrc(orc)
cs.readScore(sco)
cs.start()
pt = csound.CsoundPerformanceThread(cs.csound())
pt.play()
fig, ax = plt.subplots()
ax.set(xlim=(0,5), ylim=(0,1))
line, = ax.plot([], [], lw=2)
def animate(i, x=[], y=[]):
x.append(i/10)
y.append(cs.controlChannel('val')[0])
line.set_data(x, y)
anim = animation.FuncAnimation(fig, animate, interval=100)
You should see more or less the same here: a line starting from (0,0) to (5,1).
Well, more or less ... --- Depending on the time the backend needs to create the canvas, your line will be shifted a bit . A simple way to deal with it is to start the first instrument a bit later. In my case. 0.2 instead of 0 is a good option.
Remember to execute these commands before you run the example again:
In [4]:
pt.stop()
pt.join()
cs.reset()
The next version applies some more consistency to the variable settings. You can set any frame rate in milliseconds in the tmint
variable. And the x-axis will shift if the time has reached 4/5 of its size. So you can watch how the line moves as long as your instrument duration allows ...
In [5]:
import ctcsound as csound
from matplotlib import pyplot as plt
from matplotlib import animation
orc = '''
ksmps = 128
seed 0
instr 1
kVal randomi 0, 1, 1, 3
chnset kVal, "val"
endin
'''
sco = "i1 0.2 99999\n"
#plot and animation settings
xlim=(0,5)
ylim=(0,1)
tmint = 100 #time interval in ms
cschn = 'val' #csound channel name
cs = csound.Csound()
cs.setOption('-odac')
cs.compileOrc(orc)
cs.readScore(sco)
cs.start()
pt = csound.CsoundPerformanceThread(cs.csound())
pt.play()
fig, ax = plt.subplots()
ax.set(xlim=xlim, ylim=ylim)
line, = ax.plot([], [], lw=2)
fps = 1000/tmint
xrange = xlim[1] - xlim[0]
xshow = 4/5
xclear = 1-xshow
def animate(i, x=[], y=[]):
x.append(i/fps)
y.append(cs.controlChannel(cschn)[0])
line.set_data(x, y)
if i > fps*xrange*xshow:
ax.set_xlim(i/fps-xrange*xshow,i/fps+xrange*xclear)
anim = animation.FuncAnimation(fig, animate, interval=tmint)
In [6]:
pt.stop()
pt.join()
cs.reset()
The goal of the approach here is not to have live video for a musical performance, but to use the nice features of matplotlib for showing how a control signal is moving. But it seems that even for simple sounding examples it works, as the example below suggests.
There are a number of optimizations which I have not used. If necessary, they should improve the performance:
blit=True
can save some speed (in this case, the init and the animate function must return the line,
variable then).
In [7]:
import ctcsound as csound
from matplotlib import pyplot as plt
from matplotlib import animation
orc = '''
ksmps = 128
nchnls = 2
0dbfs = 1
seed 0
instr 1
kMidiPitch randomi 57, 62, 1, 3
kVibr = poscil:k(randomi:k(0,1,.2,3),randomi:k(3,8,1))
kDb randomi -20, 0, 1/3, 3
kPan randomi 0, 1, 1, 3
chnset kMidiPitch, "pitch"
chnset kDb, "vol"
chnset kPan, "pan"
aSnd vco2 ampdb(kDb), mtof(kMidiPitch+kVibr)
aL, aR pan2 aSnd, kPan
out aL, aR
endin
'''
sco = "i1 0.2 99999\n"
xlim_pv=(0,5)
xlim_pan=(0,1)
ylim_pch=(57,62)
ylim_vol=(-20,0)
ylim_pan=(0,0.2)
tmint = 100
chn_pch = 'pitch'
chn_vol = 'vol'
chn_pan = 'pan'
cs = csound.Csound()
cs.setOption('-odac')
cs.compileOrc(orc)
cs.readScore(sco)
cs.start()
pt = csound.CsoundPerformanceThread(cs.csound())
pt.play()
fig, ax = plt.subplots(3, tight_layout=True, gridspec_kw={'height_ratios': [3, 3, 1]})
ax[0].set(xlim=xlim_pv, ylim=ylim_pch, title='Pitch', xticks=())
ax[1].set(xlim=xlim_pv, ylim=ylim_vol, title='Volume (dB)', xticks=())
ax[2].set(xlim=xlim_pan, ylim=ylim_pan, title='Pan', xticks=[0,0.5,1], xticklabels=['L','M','R'], yticks=())
ax[0].spines['top'].set_visible(False)
ax[1].spines['top'].set_visible(False)
ax[2].spines['top'].set_visible(False)
ax[0].spines['right'].set_visible(False)
ax[1].spines['right'].set_visible(False)
ax[2].spines['right'].set_visible(False)
ax[2].spines['left'].set_visible(False)
pchline, = ax[0].plot([], [], lw=2, c='r')
volline, = ax[1].plot([], [], lw=2, c='b')
panpnt, = ax[2].plot(0.5, 0.1, 'go', lw=4)
fps = 1000/tmint
xrange = xlim_pv[1] - xlim_pv[0]
xshow = 4/5
xclear = 1-xshow
def animate(i, x_pv=[], y_pch=[], y_vol=[]):
x_pv.append(i/fps)
y_pch.append(cs.controlChannel(chn_pch)[0])
pchline.set_data(x_pv, y_pch)
y_vol.append(cs.controlChannel(chn_vol)[0])
volline.set_data(x_pv, y_vol)
if i > fps*xrange*xshow:
ax[0].set_xlim(i/fps-xrange*xshow,i/fps+xrange*xclear)
ax[1].set_xlim(i/fps-xrange*xshow,i/fps+xrange*xclear)
x_pan = cs.controlChannel(chn_pan)[0]
panpnt.set_data(x_pan,0.1)
anim = animation.FuncAnimation(fig, animate, interval=tmint)
In [8]:
pt.stop()
pt.join()
cs.reset()